diff --git a/src/librustdoc/html/static/js/search.js b/src/librustdoc/html/static/js/search.js index fa0d5d51b466..443b7e366d4d 100644 --- a/src/librustdoc/html/static/js/search.js +++ b/src/librustdoc/html/static/js/search.js @@ -173,7 +173,14 @@ window.initSearch = function(rawSearchIndex) { function isStopCharacter(c) { return isWhitespace(c) || "),>-=".indexOf(c) !== -1; } - function getStringElem(query) { + function getStringElem(query, isInGenerics) { + if (isInGenerics) { + throw new Error("`\"` cannot be used in generics"); + } else if (query.literalSearch) { + throw new Error("Cannot have more than one literal search element"); + } else if (query.totalElems !== 0) { + throw new Error("Cannot use literal search when there is more than one element"); + } query.pos += 1; while (query.pos < query.length && query.val[query.pos] !== "\"") { if (query.val[query.pos] === "\\") { @@ -182,24 +189,25 @@ window.initSearch = function(rawSearchIndex) { } query.pos += 1; } + if (query.pos >= query.length) { + throw new Error("Unclosed `\"`"); + } // To skip the quote at the end. query.pos += 1; + query.literalSearch = true; } function skipWhitespaces(query) { - var c; while (query.pos < query.length) { - c = query.val[query.pos]; + var c = query.val[query.pos]; if (!isWhitespace(c)) { break; } query.pos += 1; } - } function skipStopCharacters(query) { - var c; while (query.pos < query.length) { - c = query.val[query.pos]; + var c = query.val[query.pos]; if (!isStopCharacter(c)) { break; } @@ -222,7 +230,7 @@ window.initSearch = function(rawSearchIndex) { } } } - function createQueryElement(elems, val, generics, isExact) { + function createQueryElement(query, elems, val, generics) { removeEmptyStringsFromArray(generics); if (val === '*' || (val.length === 0 && generics.length === 0)) { return; @@ -234,16 +242,15 @@ window.initSearch = function(rawSearchIndex) { paths = [""]; } elems.push({ - isExact: isExact, name: val, fullPath: paths, pathWithoutLast: paths.slice(0, paths.length - 1), pathLast: paths[paths.length - 1], generics: generics, }); + query.totalElems += 1; } - function getNextElem(query, elems) { - var isExact = false; + function getNextElem(query, elems, isInGenerics) { var generics = []; skipStopCharacters(query); @@ -251,9 +258,8 @@ window.initSearch = function(rawSearchIndex) { var end = start; // We handle the strings on their own mostly to make code easier to follow. if (query.val[query.pos] === "\"") { - isExact = true; start += 1; - getStringElem(query); + getStringElem(query, isInGenerics); end = query.pos - 1; skipWhitespaces(query); } else { @@ -281,20 +287,18 @@ window.initSearch = function(rawSearchIndex) { if (start >= end && generics.length === 0) { return; } - createQueryElement(elems, query.val.slice(start, end), generics, isExact); + createQueryElement(query, elems, query.val.slice(start, end), generics); } function getItemsBefore(query, elems, limit) { - var c; - while (query.pos < query.length) { - c = query.val[query.pos]; + var c = query.val[query.pos]; if (c === limit) { break; } else if (isSpecialStartCharacter(c) || c === ":") { // Something weird is going on in here. Ignoring it! query.pos += 1; } - getNextElem(query, elems); + getNextElem(query, elems, limit === ">"); } // We skip the "limit". query.pos += 1; @@ -319,10 +323,12 @@ window.initSearch = function(rawSearchIndex) { // The type filter doesn't count as an element since it's a modifier. query.typeFilter = query.elems.pop().name; query.pos += 1; + query.totalElems = 0; + query.literalSearch = false; continue; } before = query.elems.length; - getNextElem(query, query.elems); + getNextElem(query, query.elems, false); if (query.elems.length === before) { // Nothing was added, let's check it's not because of a solo ":"! if (query.pos >= query.length || query.val[query.pos] !== ":") { @@ -369,20 +375,39 @@ window.initSearch = function(rawSearchIndex) { elemName: null, args: [], returned: [], + // Total number of elements (includes generics). + totalElems: 0, + // Total number of "top" elements (does not include generics). foundElems: 0, // This field is used to check if it's needed to re-run a search or not. id: "", // This field is used in `sortResults`. nameSplit: null, + literalSearch: false, + error: null, }; - parseInput(query); + query.id = val; + try { + parseInput(query); + } catch (err) { + query.error = err.message; + query.elems = []; + query.returned = []; + query.args = []; + return query; + } query.foundElems = query.elems.length + query.args.length + query.returned.length; + if (!query.literalSearch) { + // If there is more than one element in the query, we switch to literalSearch in any + // case. + query.literalSearch = query.foundElems > 1; + } if (query.elemName !== null) { query.foundElems += 1; } if (query.foundElems === 0 && val.length !== 0) { // In this case, we'll simply keep whatever was entered by the user... - createQueryElement(query.elems, val, [], false); + createQueryElement(query, query.elems, val, []); query.foundElems += 1; } if (query.typeFilter !== null) { @@ -391,7 +416,6 @@ window.initSearch = function(rawSearchIndex) { } else { query.typeFilter = NO_TYPE_FILTER; } - query.id = val; // In case we only have one argument, we move it back to `elems` to keep things simple. if (query.foundElems === 1 && query.elemName !== null) { query.elems.push(query.elemName); @@ -404,15 +428,36 @@ window.initSearch = function(rawSearchIndex) { return query; } + /** + * Creates the query results. + * + * @param {Array} results_in_args + * @param {Array} results_returned + * @param {Array} results_in_args + * @param {ParsedQuery} queryInfo + * @return {Object} - A search index of results + */ + function createQueryResults(results_in_args, results_returned, results_others, queryInfo) { + return { + "in_args": results_in_args, + "returned": results_returned, + "others": results_others, + "query": queryInfo, + }; + } + /** * Executes the query and builds an index of results - * @param {[Object]} query [The user query] - * @param {[type]} searchWords [The list of search words to query - * against] - * @param {[type]} filterCrates [Crate to search in if defined] - * @return {[type]} [A search index of results] + * + * @param {ParsedQuery} query - The user query + * @param {Object} searchWords - The list of search words to query against + * @param {Object} filterCrates - Crate to search in if defined + * @return {Object} - A search index of results */ function execQuery(queryInfo, searchWords, filterCrates) { + if (queryInfo.error !== null) { + createQueryResults([], [], [], queryInfo); + } var results_others = {}, results_in_args = {}, results_returned = {}; function transformResults(results) { @@ -618,7 +663,7 @@ window.initSearch = function(rawSearchIndex) { var lev = MAX_LEV_DISTANCE + 1; for (var x = 0, length = obj[GENERICS_DATA].length; x < length && lev !== 0; ++x) { lev = Math.min( - checkType(obj[GENERICS_DATA][x], val), + checkType(obj[GENERICS_DATA][x], val, true), lev ); } @@ -629,13 +674,14 @@ window.initSearch = function(rawSearchIndex) { * This function checks if the object (`obj`) matches the given type (`val`) and its * generics (if any). * - * @param {Object} obj - * @param {Object} val + * @param {Row} obj + * @param {QueryElement} val - The element from the parsed query. + * @param {boolean} literalSearch * * @return {integer} - Returns a Levenshtein distance to the best match. If there is * no match, returns `MAX_LEV_DISTANCE + 1`. */ - function checkType(obj, val) { + function checkType(obj, val, literalSearch) { if (val.name.length === 0 || obj[NAME].length === 0) { // This is a pure "generic" search, no need to run other checks. if (obj.length > GENERICS_DATA) { @@ -645,7 +691,7 @@ window.initSearch = function(rawSearchIndex) { } var lev = levenshtein(obj[NAME], val.name); - if (val.isExact) { + if (literalSearch) { if (lev !== 0) { // The name didn't match, let's try to check if the generics do. if (val.generics.length === 0) { @@ -720,13 +766,13 @@ window.initSearch = function(rawSearchIndex) { if (!typePassesFilter(typeFilter, tmp[1])) { continue; } - lev = Math.min(lev, checkType(tmp, val)); + lev = Math.min(lev, checkType(tmp, val, queryInfo.literalSearch)); if (lev === 0) { return 0; } } } - return val.isExact ? MAX_LEV_DISTANCE + 1 : lev; + return queryInfo.literalSearch ? MAX_LEV_DISTANCE + 1 : lev; } /** @@ -751,13 +797,13 @@ window.initSearch = function(rawSearchIndex) { if (!typePassesFilter(typeFilter, tmp[1])) { continue; } - lev = Math.min(lev, checkType(tmp, val)); + lev = Math.min(lev, checkType(tmp, val, queryInfo.literalSearch)); if (lev === 0) { return 0; } } } - return val.isExact ? MAX_LEV_DISTANCE + 1 : lev; + return queryInfo.literalSearch ? MAX_LEV_DISTANCE + 1 : lev; } function checkPath(contains, lastElem, ty) { @@ -888,7 +934,7 @@ window.initSearch = function(rawSearchIndex) { * This function adds the given result into the provided `res` map if it matches the * following condition: * - * * If it is a "literal search" (`isExact`), then `lev` must be 0. + * * If it is a "literal search" (`queryInfo.literalSearch`), then `lev` must be 0. * * If it is not a "literal search", `lev` must be <= `MAX_LEV_DISTANCE`. * * The `res` map contains information which will be used to sort the search results: @@ -898,15 +944,14 @@ window.initSearch = function(rawSearchIndex) { * * `index` is an `integer`` used to sort by the position of the word in the item's name. * * `lev` is the main metric used to sort the search results. * - * @param {boolean} isExact * @param {Object} res * @param {string} fullId * @param {integer} id * @param {integer} index * @param {integer} lev */ - function addIntoResults(isExact, res, fullId, id, index, lev) { - if (lev === 0 || (!isExact && lev <= MAX_LEV_DISTANCE)) { + function addIntoResults(res, fullId, id, index, lev) { + if (lev === 0 || (!queryInfo.literalSearch && lev <= MAX_LEV_DISTANCE)) { if (res[fullId] !== undefined) { var result = res[fullId]; if (result.dontValidate || result.lev <= lev) { @@ -916,7 +961,7 @@ window.initSearch = function(rawSearchIndex) { res[fullId] = { id: id, index: index, - dontValidate: isExact, + dontValidate: queryInfo.literalSearch, lev: lev, }; } @@ -939,17 +984,17 @@ window.initSearch = function(rawSearchIndex) { var in_args = findArg(ty, elem, queryInfo.typeFilter); var returned = checkReturned(ty, elem, queryInfo.typeFilter); - addIntoResults(elem.isExact, results_in_args, fullId, pos, index, in_args); - addIntoResults(elem.isExact, results_returned, fullId, pos, index, returned); + addIntoResults(results_in_args, fullId, pos, index, in_args); + addIntoResults(results_returned, fullId, pos, index, returned); if (!typePassesFilter(queryInfo.typeFilter, ty.ty)) { return; } var searchWord = searchWords[pos]; - if (elem.isExact) { + if (queryInfo.literalSearch) { if (searchWord === elem.name) { - addIntoResults(true, results_others, fullId, pos, -1, 0); + addIntoResults(results_others, fullId, pos, -1, 0); } return; } @@ -958,14 +1003,14 @@ window.initSearch = function(rawSearchIndex) { if (elem.name.length === 0) { if (ty.type !== null) { lev = checkGenerics(ty.type, elem, MAX_LEV_DISTANCE + 1); - addIntoResults(false, results_others, fullId, pos, index, lev); + addIntoResults(results_others, fullId, pos, index, lev); } return; } if (elem.fullPath.length > 1) { lev = checkPath(elem.pathWithoutLast, elem.pathLast, ty); - if (lev > MAX_LEV_DISTANCE || (elem.isExact && lev !== 0)) { + if (lev > MAX_LEV_DISTANCE || (queryInfo.literalSearch && lev !== 0)) { return; } else if (lev > 0) { lev_add = lev / 10; @@ -998,7 +1043,7 @@ window.initSearch = function(rawSearchIndex) { if (lev < 0) { lev = 0; } - addIntoResults(elem.isExact, results_others, fullId, pos, index, lev); + addIntoResults(results_others, fullId, pos, index, lev); } /** @@ -1024,7 +1069,6 @@ window.initSearch = function(rawSearchIndex) { for (i = 0, len = args.length; i < len; ++i) { el = args[i]; // There is more than one parameter to the query so all checks should be "exact" - el.isExact = true; lev = callback(ty, el, NO_TYPE_FILTER); if (lev <= 1) { nbLev += 1; @@ -1049,7 +1093,7 @@ window.initSearch = function(rawSearchIndex) { return; } lev = Math.round(totalLev / nbLev); - addIntoResults(false, results, ty.id, pos, 0, lev); + addIntoResults(results, ty.id, pos, 0, lev); } function innerRunQuery() { @@ -1069,7 +1113,7 @@ window.initSearch = function(rawSearchIndex) { for (i = 0, nSearchWords = searchWords.length; i < nSearchWords; ++i) { ty = searchIndex[i]; in_args = findArg(ty, elem, queryInfo.typeFilter); - addIntoResults(elem.isExact, results_in_args, ty.id, i, -1, in_args); + addIntoResults(results_in_args, ty.id, i, -1, in_args); } } else if (queryInfo.returned.length === 1) { // We received one returned argument to check, so looking into returned values. @@ -1077,7 +1121,7 @@ window.initSearch = function(rawSearchIndex) { for (i = 0, nSearchWords = searchWords.length; i < nSearchWords; ++i) { ty = searchIndex[i]; in_returned = checkReturned(ty, elem, queryInfo.typeFilter); - addIntoResults(elem.isExact, results_returned, ty.id, i, -1, in_returned); + addIntoResults(results_returned, ty.id, i, -1, in_returned); } } } else if (queryInfo.foundElems > 0) { @@ -1096,12 +1140,11 @@ window.initSearch = function(rawSearchIndex) { } innerRunQuery(); - var ret = { - "in_args": sortResults(results_in_args, true), - "returned": sortResults(results_returned, true), - "others": sortResults(results_others, false), - "query": queryInfo, - }; + var ret = createQueryResults( + sortResults(results_in_args, true), + sortResults(results_returned, true), + sortResults(results_others, false), + queryInfo); handleAliases(ret, queryInfo.original.replace(/"/g, "").toLowerCase(), filterCrates); return ret; } @@ -1241,7 +1284,7 @@ window.initSearch = function(rawSearchIndex) { var output = document.createElement("div"); var length = 0; - if (array.length > 0) { + if (array.length > 0 && query.error === null) { output.className = "search-results " + extraClass; array.forEach(function(item) { @@ -1294,6 +1337,9 @@ window.initSearch = function(rawSearchIndex) { link.appendChild(wrapper); output.appendChild(link); }); + } else if (query.error !== null) { + output.className = "search-failed" + extraClass; + output.innerHTML = "Syntax error: " + query.error; } else { output.className = "search-failed" + extraClass; output.innerHTML = "No results :(
" +