diff --git a/Cargo.lock b/Cargo.lock index 0d37e8b2f84c0..65f57ee54f748 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4725,6 +4725,7 @@ dependencies = [ "serde_json", "sha2", "smallvec", + "stringdex", "tempfile", "threadpool", "tracing", @@ -5132,6 +5133,15 @@ dependencies = [ "quote", ] +[[package]] +name = "stringdex" +version = "0.0.1-alpha1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a9ed1635ee8462c251e9c85aa1dd345419bd004bb75ac9a96e4c442a7974683" +dependencies = [ + "stacker", +] + [[package]] name = "strsim" version = "0.11.1" diff --git a/package-lock.json b/package-lock.json index dbdd9c644f149..1c8df15924c95 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,7 +7,7 @@ "dependencies": { "browser-ui-test": "^0.21.1", "es-check": "^6.2.1", - "eslint": "^8.57.1", + "eslint": "^8.6.0", "eslint-js": "github:eslint/js", "typescript": "^5.8.3" } @@ -79,42 +79,15 @@ "kuler": "^2.0.0" } }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.4.1.tgz", + "integrity": "sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA==", "license": "MIT", "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.6.0", + "espree": "^9.4.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", @@ -129,47 +102,34 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@eslint/js": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", - "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "license": "MIT", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">= 4" } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", - "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "version": "0.9.5", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz", + "integrity": "sha512-ObyMyWxZiCu/yTisA7uzx81s40xR2fD5Cg/2Kq7G02ajkNubJf6BopgDTmDyc3U7sXpNKM8cYOw7s7Tyr+DnCw==", "deprecated": "Use @eslint/config-array instead", "license": "Apache-2.0", "dependencies": { - "@humanwhocodes/object-schema": "^2.0.3", - "debug": "^4.3.1", - "minimatch": "^3.0.5" + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.4" }, "engines": { "node": ">=10.10.0" } }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "deprecated": "Use @eslint/object-schema instead", "license": "BSD-3-Clause" }, @@ -301,12 +261,6 @@ "@types/node": "*" } }, - "node_modules/@ungap/structured-clone": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", - "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", - "license": "ISC" - }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -353,6 +307,15 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/ansi-escapes": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", @@ -922,6 +885,19 @@ "once": "^1.4.0" } }, + "node_modules/enquirer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/env-paths": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", @@ -1006,50 +982,50 @@ } }, "node_modules/eslint": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", - "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.6.0.tgz", + "integrity": "sha512-UvxdOJ7mXFlw7iuHZA4jmzPaUqIw54mZrv+XPYKNbKdLR0et4rf60lIZUU9kiNtnzzMzGWxMV+tQ7uG7JG8DPw==", "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.1", - "@humanwhocodes/config-array": "^0.13.0", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", + "@eslint/eslintrc": "^1.0.5", + "@humanwhocodes/config-array": "^0.9.2", + "ajv": "^6.10.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", + "enquirer": "^2.3.5", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", + "eslint-scope": "^7.1.0", + "eslint-utils": "^3.0.0", + "eslint-visitor-keys": "^3.1.0", + "espree": "^9.3.0", + "esquery": "^1.4.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^6.0.1", + "globals": "^13.6.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", + "minimatch": "^3.0.4", "natural-compare": "^1.4.0", - "optionator": "^0.9.3", + "optionator": "^0.9.1", + "progress": "^2.0.0", + "regexpp": "^3.2.0", + "semver": "^7.2.1", "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" + "strip-json-comments": "^3.1.0", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" }, "bin": { "eslint": "bin/eslint.js" @@ -1084,6 +1060,33 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^2.0.0" + }, + "engines": { + "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=5" + } + }, + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "license": "Apache-2.0", + "engines": { + "node": ">=10" + } + }, "node_modules/eslint-visitor-keys": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", @@ -1339,22 +1342,6 @@ "node": ">=8" } }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/flat-cache": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", @@ -1387,6 +1374,12 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "license": "ISC" }, + "node_modules/functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "license": "MIT" + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -1473,12 +1466,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "license": "MIT" - }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -1547,9 +1534,9 @@ "license": "BSD-3-Clause" }, "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", "license": "MIT", "engines": { "node": ">= 4" @@ -1805,15 +1792,6 @@ "node": ">=0.12.0" } }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -1914,21 +1892,6 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "license": "MIT" }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -2120,36 +2083,6 @@ "node": ">=0.10.0" } }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/pac-proxy-agent": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", @@ -2212,15 +2145,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -2412,6 +2336,18 @@ "node": ">= 0.8.0" } }, + "node_modules/regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -3002,6 +2938,12 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/v8-compile-cache": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.4.0.tgz", + "integrity": "sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw==", + "license": "MIT" + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -3207,18 +3149,6 @@ "fd-slicer": "~1.1.0" } }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/zod": { "version": "3.23.8", "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", diff --git a/package.json b/package.json index 945f9a3ee87dd..48a6f69fb4bd7 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "dependencies": { "browser-ui-test": "^0.21.1", "es-check": "^6.2.1", - "eslint": "^8.57.1", + "eslint": "^8.6.0", "eslint-js": "github:eslint/js", "typescript": "^5.8.3" } diff --git a/src/ci/docker/host-x86_64/tidy/eslint.version b/src/ci/docker/host-x86_64/tidy/eslint.version index 1acea15afd690..acd405b1d62e3 100644 --- a/src/ci/docker/host-x86_64/tidy/eslint.version +++ b/src/ci/docker/host-x86_64/tidy/eslint.version @@ -1 +1 @@ -8.6.0 \ No newline at end of file +8.6.0 diff --git a/src/etc/htmldocck.py b/src/etc/htmldocck.py index 72975cc6206c0..8d7f7341c2e65 100755 --- a/src/etc/htmldocck.py +++ b/src/etc/htmldocck.py @@ -15,6 +15,7 @@ import re import shlex from collections import namedtuple +from pathlib import Path try: from html.parser import HTMLParser @@ -242,6 +243,11 @@ def resolve_path(self, path): return self.last_path def get_absolute_path(self, path): + if "*" in path: + paths = list(Path(self.root).glob(path)) + if len(paths) != 1: + raise FailedCheck("glob path does not resolve to one file") + path = str(paths[0]) return os.path.join(self.root, path) def get_file(self, path): diff --git a/src/librustdoc/Cargo.toml b/src/librustdoc/Cargo.toml index fdde8309cf9f2..ad6c1dd05d577 100644 --- a/src/librustdoc/Cargo.toml +++ b/src/librustdoc/Cargo.toml @@ -21,6 +21,7 @@ rustdoc-json-types = { path = "../rustdoc-json-types" } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" smallvec = "1.8.1" +stringdex = "0.0.1-alpha1" tempfile = "3" threadpool = "1.8.1" tracing = "0.1" diff --git a/src/librustdoc/build.rs b/src/librustdoc/build.rs index 07269d5bdc240..5b497183ae602 100644 --- a/src/librustdoc/build.rs +++ b/src/librustdoc/build.rs @@ -10,6 +10,7 @@ fn main() { "static/css/normalize.css", "static/js/main.js", "static/js/search.js", + "static/js/stringdex.js", "static/js/settings.js", "static/js/src-script.js", "static/js/storage.js", diff --git a/src/librustdoc/formats/cache.rs b/src/librustdoc/formats/cache.rs index 5191120ebdb0f..0fa0b8163ff2d 100644 --- a/src/librustdoc/formats/cache.rs +++ b/src/librustdoc/formats/cache.rs @@ -1,6 +1,5 @@ use std::mem; -use rustc_ast::join_path_syms; use rustc_attr_data_structures::StabilityLevel; use rustc_data_structures::fx::{FxHashMap, FxHashSet, FxIndexMap, FxIndexSet}; use rustc_hir::def_id::{CrateNum, DefId, DefIdMap, DefIdSet}; @@ -558,7 +557,6 @@ fn add_item_to_search_index(tcx: TyCtxt<'_>, cache: &mut Cache, item: &clean::It clean::ItemKind::ImportItem(import) => import.source.did.unwrap_or(item_def_id), _ => item_def_id, }; - let path = join_path_syms(parent_path); let impl_id = if let Some(ParentStackItem::Impl { item_id, .. }) = cache.parent_stack.last() { item_id.as_def_id() } else { @@ -577,11 +575,11 @@ fn add_item_to_search_index(tcx: TyCtxt<'_>, cache: &mut Cache, item: &clean::It ty: item.type_(), defid: Some(defid), name, - path, + module_path: parent_path.to_vec(), desc, parent: parent_did, parent_idx: None, - exact_path: None, + exact_module_path: None, impl_id, search_type, aliases, diff --git a/src/librustdoc/formats/item_type.rs b/src/librustdoc/formats/item_type.rs index 3aba7a370adb2..a41e0e7cec112 100644 --- a/src/librustdoc/formats/item_type.rs +++ b/src/librustdoc/formats/item_type.rs @@ -4,7 +4,7 @@ use std::fmt; use rustc_hir::def::{CtorOf, DefKind}; use rustc_span::hygiene::MacroKind; -use serde::{Serialize, Serializer}; +use serde::{Deserialize, Deserializer, Serialize, Serializer, de}; use crate::clean; @@ -68,6 +68,52 @@ impl Serialize for ItemType { } } +impl<'de> Deserialize<'de> for ItemType { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct ItemTypeVisitor; + impl<'de> de::Visitor<'de> for ItemTypeVisitor { + type Value = ItemType; + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "an integer between 0 and 25") + } + fn visit_u64(self, v: u64) -> Result { + Ok(match v { + 0 => ItemType::Keyword, + 1 => ItemType::Primitive, + 2 => ItemType::Module, + 3 => ItemType::ExternCrate, + 4 => ItemType::Import, + 5 => ItemType::Struct, + 6 => ItemType::Enum, + 7 => ItemType::Function, + 8 => ItemType::TypeAlias, + 9 => ItemType::Static, + 10 => ItemType::Trait, + 11 => ItemType::Impl, + 12 => ItemType::TyMethod, + 13 => ItemType::Method, + 14 => ItemType::StructField, + 15 => ItemType::Variant, + 16 => ItemType::Macro, + 17 => ItemType::AssocType, + 18 => ItemType::Constant, + 19 => ItemType::AssocConst, + 20 => ItemType::Union, + 21 => ItemType::ForeignType, + 23 => ItemType::ProcAttribute, + 24 => ItemType::ProcDerive, + 25 => ItemType::TraitAlias, + _ => return Err(E::missing_field("unknown number")), + }) + } + } + deserializer.deserialize_any(ItemTypeVisitor) + } +} + impl<'a> From<&'a clean::Item> for ItemType { fn from(item: &'a clean::Item) -> ItemType { let kind = match &item.kind { diff --git a/src/librustdoc/html/layout.rs b/src/librustdoc/html/layout.rs index 1f92c521d46e5..10ee026256066 100644 --- a/src/librustdoc/html/layout.rs +++ b/src/librustdoc/html/layout.rs @@ -24,6 +24,7 @@ pub(crate) struct Layout { pub(crate) struct Page<'a> { pub(crate) title: &'a str, + pub(crate) short_title: &'a str, pub(crate) css_class: &'a str, pub(crate) root_path: &'a str, pub(crate) static_root_path: Option<&'a str>, diff --git a/src/librustdoc/html/render/context.rs b/src/librustdoc/html/render/context.rs index 5ceb1fc988dc9..e4fca09d64ff7 100644 --- a/src/librustdoc/html/render/context.rs +++ b/src/librustdoc/html/render/context.rs @@ -204,6 +204,18 @@ impl<'tcx> Context<'tcx> { if !is_module { title.push_str(it.name.unwrap().as_str()); } + let short_title; + let short_title = if is_module { + let module_name = self.current.last().unwrap(); + short_title = if it.is_crate() { + format!("Crate {module_name}") + } else { + format!("Module {module_name}") + }; + &short_title[..] + } else { + it.name.as_ref().unwrap().as_str() + }; if !it.is_primitive() && !it.is_keyword() { if !is_module { title.push_str(" in "); @@ -240,6 +252,7 @@ impl<'tcx> Context<'tcx> { root_path: &self.root_path(), static_root_path: self.shared.static_root_path.as_deref(), title: &title, + short_title, description: &desc, resource_suffix: &self.shared.resource_suffix, rust_logo: has_doc_flag(self.tcx(), LOCAL_CRATE.as_def_id(), sym::rust_logo), @@ -617,6 +630,7 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> { let shared = &self.shared; let mut page = layout::Page { title: "List of all items in this crate", + short_title: "All", css_class: "mod sys", root_path: "../", static_root_path: shared.static_root_path.as_deref(), diff --git a/src/librustdoc/html/render/mod.rs b/src/librustdoc/html/render/mod.rs index 872dbbcd19e17..bfbe33a009bb5 100644 --- a/src/librustdoc/html/render/mod.rs +++ b/src/librustdoc/html/render/mod.rs @@ -132,11 +132,11 @@ pub(crate) struct IndexItem { pub(crate) ty: ItemType, pub(crate) defid: Option, pub(crate) name: Symbol, - pub(crate) path: String, + pub(crate) module_path: Vec, pub(crate) desc: String, pub(crate) parent: Option, - pub(crate) parent_idx: Option, - pub(crate) exact_path: Option, + pub(crate) parent_idx: Option, + pub(crate) exact_module_path: Option>, pub(crate) impl_id: Option, pub(crate) search_type: Option, pub(crate) aliases: Box<[Symbol]>, @@ -193,6 +193,62 @@ impl RenderType { write_optional_id(self.id, string); } } + fn read_from_bytes(string: &[u8]) -> (RenderType, usize) { + let mut i = 0; + if string[i] == b'{' { + i += 1; + let (id, offset) = RenderTypeId::read_from_bytes(&string[i..]); + i += offset; + let generics = if string[i] == b'{' { + i += 1; + let mut generics = Vec::new(); + while string[i] != b'}' { + let (ty, offset) = RenderType::read_from_bytes(&string[i..]); + i += offset; + generics.push(ty); + } + assert!(string[i] == b'}'); + i += 1; + Some(generics) + } else { + None + }; + let bindings = if string[i] == b'{' { + i += 1; + let mut bindings = Vec::new(); + while string[i] == b'{' { + i += 1; + let (binding, boffset) = RenderTypeId::read_from_bytes(&string[i..]); + i += boffset; + let mut bconstraints = Vec::new(); + assert!(string[i] == b'{'); + i += 1; + while string[i] != b'}' { + let (constraint, coffset) = RenderType::read_from_bytes(&string[i..]); + i += coffset; + bconstraints.push(constraint); + } + assert!(string[i] == b'}'); + i += 1; + bindings.push((binding.unwrap(), bconstraints)); + assert!(string[i] == b'}'); + i += 1; + } + assert!(string[i] == b'}'); + i += 1; + Some(bindings) + } else { + None + }; + assert!(string[i] == b'}'); + i += 1; + (RenderType { id, generics, bindings }, i) + } else { + let (id, offset) = RenderTypeId::read_from_bytes(string); + i += offset; + (RenderType { id, generics: None, bindings: None }, i) + } + } } #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -214,7 +270,20 @@ impl RenderTypeId { RenderTypeId::Index(idx) => (*idx).try_into().unwrap(), _ => panic!("must convert render types to indexes before serializing"), }; - search_index::encode::write_vlqhex_to_string(id, string); + search_index::encode::write_signed_vlqhex_to_string(id, string); + } + fn read_from_bytes(string: &[u8]) -> (Option, usize) { + let Some((value, offset)) = search_index::encode::read_signed_vlqhex_from_string(string) + else { + return (None, 0); + }; + let value = isize::try_from(value).unwrap(); + let ty = match value { + ..0 => Some(RenderTypeId::Index(value)), + 0 => None, + 1.. => Some(RenderTypeId::Index(value - 1)), + }; + (ty, offset) } } @@ -228,12 +297,55 @@ pub(crate) struct IndexItemFunctionType { } impl IndexItemFunctionType { - fn write_to_string<'a>( - &'a self, - string: &mut String, - backref_queue: &mut VecDeque<&'a IndexItemFunctionType>, - ) { - assert!(backref_queue.len() <= 16); + fn read_from_string_without_param_names(string: &[u8]) -> (IndexItemFunctionType, usize) { + let mut i = 0; + if string[i] == b'`' { + return ( + IndexItemFunctionType { + inputs: Vec::new(), + output: Vec::new(), + where_clause: Vec::new(), + param_names: Vec::new(), + }, + 1, + ); + } + assert_eq!(b'{', string[i]); + i += 1; + fn read_args_from_string(string: &[u8]) -> (Vec, usize) { + let mut i = 0; + let mut params = Vec::new(); + if string[i] == b'{' { + // multiple params + i += 1; + while string[i] != b'}' { + let (ty, offset) = RenderType::read_from_bytes(&string[i..]); + i += offset; + params.push(ty); + } + i += 1; + } else if string[i] != b'}' { + let (tyid, offset) = RenderTypeId::read_from_bytes(&string[i..]); + params.push(RenderType { id: tyid, generics: None, bindings: None }); + i += offset; + } + (params, i) + } + let (inputs, offset) = read_args_from_string(&string[i..]); + i += offset; + let (output, offset) = read_args_from_string(&string[i..]); + i += offset; + let mut where_clause = Vec::new(); + while string[i] != b'}' { + let (constraint, offset) = read_args_from_string(&string[i..]); + i += offset; + where_clause.push(constraint); + } + assert_eq!(b'}', string[i], "{} {}", String::from_utf8_lossy(&string), i); + i += 1; + (IndexItemFunctionType { inputs, output, where_clause, param_names: Vec::new() }, i) + } + fn write_to_string_without_param_names<'a>(&'a self, string: &mut String) { // If we couldn't figure out a type, just write 0, // which is encoded as `` ` `` (see RenderTypeId::write_to_string). let has_missing = self @@ -243,18 +355,7 @@ impl IndexItemFunctionType { .any(|i| i.id.is_none() && i.generics.is_none()); if has_missing { string.push('`'); - } else if let Some(idx) = backref_queue.iter().position(|other| *other == self) { - // The backref queue has 16 items, so backrefs use - // a single hexit, disjoint from the ones used for numbers. - string.push( - char::try_from('0' as u32 + u32::try_from(idx).unwrap()) - .expect("last possible value is '?'"), - ); } else { - backref_queue.push_front(self); - if backref_queue.len() > 16 { - backref_queue.pop_back(); - } string.push('{'); match &self.inputs[..] { [one] if one.generics.is_none() && one.bindings.is_none() => { diff --git a/src/librustdoc/html/render/print_item.rs b/src/librustdoc/html/render/print_item.rs index 02ee34aaac680..018bc38863010 100644 --- a/src/librustdoc/html/render/print_item.rs +++ b/src/librustdoc/html/render/print_item.rs @@ -35,6 +35,7 @@ use crate::html::format::{ visibility_print_with_space, }; use crate::html::markdown::{HeadingOffset, MarkdownSummaryLine}; +use crate::html::render::sidebar::filters; use crate::html::render::{document_full, document_item_info}; use crate::html::url_parts_builder::UrlPartsBuilder; diff --git a/src/librustdoc/html/render/search_index.rs b/src/librustdoc/html/render/search_index.rs index e2f86b8a8549e..db6034cf1f31c 100644 --- a/src/librustdoc/html/render/search_index.rs +++ b/src/librustdoc/html/render/search_index.rs @@ -1,72 +1,1128 @@ pub(crate) mod encode; +use std::collections::BTreeSet; use std::collections::hash_map::Entry; -use std::collections::{BTreeMap, VecDeque}; +use std::path::Path; -use encode::{bitmap_to_string, write_vlqhex_to_string}; use rustc_ast::join_path_syms; -use rustc_data_structures::fx::{FxHashMap, FxIndexMap}; +use rustc_data_structures::fx::{FxHashMap, FxHashSet, FxIndexMap}; use rustc_middle::ty::TyCtxt; use rustc_span::def_id::DefId; use rustc_span::sym; use rustc_span::symbol::{Symbol, kw}; -use serde::ser::{Serialize, SerializeSeq, SerializeStruct, Serializer}; +use serde::de::{self, Deserializer, Error as _}; +use serde::ser::{SerializeSeq, Serializer}; +use serde::{Deserialize, Serialize}; +use stringdex::internals as stringdex_internals; +use stringdex::internals::decode::read_base64_from_bytes; +use stringdex_internals::decode::RoaringBitmap; use thin_vec::ThinVec; use tracing::instrument; use crate::clean::types::{Function, Generics, ItemId, Type, WherePredicate}; use crate::clean::{self, utils}; +use crate::error::Error; use crate::formats::cache::{Cache, OrphanImplItem}; use crate::formats::item_type::ItemType; use crate::html::markdown::short_markdown_summary; -use crate::html::render::ordered_json::OrderedJson; use crate::html::render::{self, IndexItem, IndexItemFunctionType, RenderType, RenderTypeId}; -/// The serialized search description sharded version -/// -/// The `index` is a JSON-encoded list of names and other information. -/// -/// The desc has newlined descriptions, split up by size into 128KiB shards. -/// For example, `(4, "foo\nbar\nbaz\nquux")`. -/// -/// There is no single, optimal size for these shards, because it depends on -/// configuration values that we can't predict or control, such as the version -/// of HTTP used (HTTP/1.1 would work better with larger files, while HTTP/2 -/// and 3 are more agnostic), transport compression (gzip, zstd, etc), whether -/// the search query is going to produce a large number of results or a small -/// number, the bandwidth delay product of the network... -/// -/// Gzipping some standard library descriptions to guess what transport -/// compression will do, the compressed file sizes can be as small as 4.9KiB -/// or as large as 18KiB (ignoring the final 1.9KiB shard of leftovers). -/// A "reasonable" range for files is for them to be bigger than 1KiB, -/// since that's about the amount of data that can be transferred in a -/// single TCP packet, and 64KiB, the maximum amount of data that -/// TCP can transfer in a single round trip without extensions. -/// -/// [1]: https://en.wikipedia.org/wiki/Maximum_transmission_unit#MTUs_for_common_media -/// [2]: https://en.wikipedia.org/wiki/Sliding_window_protocol#Basic_concept -/// [3]: https://learn.microsoft.com/en-us/troubleshoot/windows-server/networking/description-tcp-features +#[derive(Clone, Debug, Default, Deserialize, Serialize)] pub(crate) struct SerializedSearchIndex { - pub(crate) index: OrderedJson, - pub(crate) desc: Vec<(usize, String)>, + // data from disk + names: Vec, + path_data: Vec>, + entry_data: Vec>, + descs: Vec, + function_data: Vec>, + alias_pointers: Vec>, + // inverted index for concrete types and generics + type_data: Vec>, + generic_inverted_index: Vec>, + // generated in-memory backref cache + #[serde(skip)] + crate_paths_index: FxHashMap<(ItemType, Vec), usize>, +} + +impl SerializedSearchIndex { + fn load(doc_root: &Path, resource_suffix: &str) -> Result { + let mut names: Vec = Vec::new(); + let mut path_data: Vec> = Vec::new(); + let mut entry_data: Vec> = Vec::new(); + let mut descs: Vec = Vec::new(); + let mut function_data: Vec> = Vec::new(); + let mut type_data: Vec> = Vec::new(); + let mut alias_pointers: Vec> = Vec::new(); + + let mut generic_inverted_index: Vec> = Vec::new(); + + match perform_read_strings(resource_suffix, doc_root, "name", &mut names) { + Ok(()) => { + perform_read_serde(resource_suffix, doc_root, "path", &mut path_data)?; + perform_read_serde(resource_suffix, doc_root, "entry", &mut entry_data)?; + perform_read_strings(resource_suffix, doc_root, "desc", &mut descs)?; + perform_read_serde(resource_suffix, doc_root, "function", &mut function_data)?; + perform_read_serde(resource_suffix, doc_root, "type", &mut type_data)?; + perform_read_serde(resource_suffix, doc_root, "alias", &mut alias_pointers)?; + perform_read_bitmap( + resource_suffix, + doc_root, + "generic_inverted_index", + &mut generic_inverted_index, + )?; + } + Err(_) => { + names.clear(); + } + } + fn perform_read_strings( + resource_suffix: &str, + doc_root: &Path, + column_name: &str, + column: &mut Vec, + ) -> Result<(), Error> { + let root_path = doc_root.join(format!("search.index/root{resource_suffix}.js")); + let column_path = doc_root.join(format!("search.index/{column_name}/")); + stringdex_internals::read_data_from_disk_column( + root_path, + column_name.as_bytes(), + column_path.clone(), + &mut |_id, item| { + column.push(String::from_utf8(item.to_vec())?); + Ok(()) + }, + ) + .map_err( + |error: stringdex_internals::ReadDataError>| Error { + file: column_path, + error: format!("failed to read column from disk: {error}"), + }, + ) + } + fn perform_read_serde( + resource_suffix: &str, + doc_root: &Path, + column_name: &str, + column: &mut Vec Deserialize<'de> + 'static>>, + ) -> Result<(), Error> { + let root_path = doc_root.join(format!("search.index/root{resource_suffix}.js")); + let column_path = doc_root.join(format!("search.index/{column_name}/")); + stringdex_internals::read_data_from_disk_column( + root_path, + column_name.as_bytes(), + column_path.clone(), + &mut |_id, item| { + if item.is_empty() { + column.push(None); + } else { + column.push(Some(serde_json::from_slice(item)?)); + } + Ok(()) + }, + ) + .map_err( + |error: stringdex_internals::ReadDataError>| Error { + file: column_path, + error: format!("failed to read column from disk: {error}"), + }, + ) + } + fn perform_read_bitmap( + resource_suffix: &str, + doc_root: &Path, + column_name: &str, + column: &mut Vec>, + ) -> Result<(), Error> { + let root_path = doc_root.join(format!("search.index/root{resource_suffix}.js")); + let column_path = doc_root.join(format!("search.index/{column_name}/")); + stringdex_internals::read_data_from_disk_column( + root_path, + column_name.as_bytes(), + column_path.clone(), + &mut |_id, item| { + let mut decoded = Vec::new(); + read_base64_from_bytes(item, &mut decoded)?; + column.push( + RoaringBitmap::from_bytes(&decoded) + .map(|(bitmap, _)| bitmap.to_vec()) + .unwrap_or_default(), + ); + Ok(()) + }, + ) + .map_err( + |error: stringdex_internals::ReadDataError>| Error { + file: column_path, + error: format!("failed to read column from disk: {error}"), + }, + ) + } + + assert_eq!(names.len(), path_data.len()); + assert_eq!(path_data.len(), entry_data.len()); + assert_eq!(entry_data.len(), descs.len()); + assert_eq!(descs.len(), function_data.len()); + assert_eq!(function_data.len(), type_data.len()); + assert_eq!(type_data.len(), alias_pointers.len()); + + // generic_inverted_index is not the same length as other columns, + // because it's actually a completely different set of objects + + let mut crate_paths_index: FxHashMap<(ItemType, Vec), usize> = FxHashMap::default(); + for (i, (name, path_data)) in names.iter().zip(path_data.iter()).enumerate() { + if let Some(path_data) = path_data { + let full_path = if path_data.module_path.is_empty() { + vec![Symbol::intern(name)] + } else { + let mut full_path = path_data.module_path.to_vec(); + full_path.push(Symbol::intern(name)); + full_path + }; + crate_paths_index.insert((path_data.ty, full_path), i); + } + } + + Ok(SerializedSearchIndex { + names, + path_data, + entry_data, + descs, + function_data, + type_data, + alias_pointers, + generic_inverted_index, + crate_paths_index, + }) + } + fn push( + &mut self, + name: String, + path_data: Option, + entry_data: Option, + desc: String, + function_data: Option, + type_data: Option, + alias_pointer: Option, + ) -> usize { + let index = self.names.len(); + assert_eq!(self.names.len(), self.path_data.len()); + if let Some(path_data) = &path_data + && let name = Symbol::intern(&name) + && let fqp = if path_data.module_path.is_empty() { + vec![name] + } else { + let mut v = path_data.module_path.clone(); + v.push(name); + v + } + && let Some(&other_path) = self.crate_paths_index.get(&(path_data.ty, fqp)) + && self.path_data.get(other_path).map_or(false, Option::is_some) + { + self.path_data.push(None); + } else { + self.path_data.push(path_data); + } + self.names.push(name); + assert_eq!(self.entry_data.len(), self.descs.len()); + self.entry_data.push(entry_data); + assert_eq!(self.descs.len(), self.function_data.len()); + self.descs.push(desc); + assert_eq!(self.function_data.len(), self.type_data.len()); + self.function_data.push(function_data); + assert_eq!(self.type_data.len(), self.alias_pointers.len()); + self.type_data.push(type_data); + self.alias_pointers.push(alias_pointer); + index + } + fn push_path(&mut self, name: String, path_data: PathData) -> usize { + self.push(name, Some(path_data), None, String::new(), None, None, None) + } + fn push_type(&mut self, name: String, path_data: PathData, type_data: TypeData) -> usize { + self.push(name, Some(path_data), None, String::new(), None, Some(type_data), None) + } + fn push_alias(&mut self, name: String, alias_pointer: usize) -> usize { + self.push(name, None, None, String::new(), None, None, Some(alias_pointer)) + } + + fn get_id_by_module_path(&mut self, path: &[Symbol]) -> usize { + let ty = if path.len() == 1 { ItemType::ExternCrate } else { ItemType::Module }; + match self.crate_paths_index.entry((ty, path.to_vec())) { + Entry::Occupied(index) => *index.get(), + Entry::Vacant(slot) => { + slot.insert(self.path_data.len()); + let (name, module_path) = path.split_last().unwrap(); + self.push_path( + name.as_str().to_string(), + PathData { ty, module_path: module_path.to_vec(), exact_module_path: None }, + ) + } + } + } + + pub(crate) fn union(mut self, other: &SerializedSearchIndex) -> SerializedSearchIndex { + let other_entryid_offset = self.names.len(); + let mut map_other_pathid_to_self_pathid: Vec = Vec::new(); + let mut skips = FxHashSet::default(); + for (other_pathid, other_path_data) in other.path_data.iter().enumerate() { + if let Some(other_path_data) = other_path_data { + let mut fqp = other_path_data.module_path.clone(); + let name = Symbol::intern(&other.names[other_pathid]); + fqp.push(name); + let self_pathid = other_entryid_offset + other_pathid; + let self_pathid = match self.crate_paths_index.entry((other_path_data.ty, fqp)) { + Entry::Vacant(slot) => { + slot.insert(self_pathid); + self_pathid + } + Entry::Occupied(existing_entryid) => { + skips.insert(other_pathid); + let self_pathid = *existing_entryid.get(); + let new_type_data = match ( + self.type_data[self_pathid].take(), + other.type_data[other_pathid].as_ref(), + ) { + (Some(self_type_data), None) => Some(self_type_data), + (None, Some(other_type_data)) => Some(TypeData { + search_unbox: other_type_data.search_unbox, + inverted_function_signature_index: other_type_data + .inverted_function_signature_index + .iter() + .copied() + .map(|fnid| { + assert!( + other.function_data[usize::try_from(fnid).unwrap()] + .is_some(), + ); + // this is valid because we call `self.push()` once, exactly, for every entry, + // even if we're just pushing a tombstone + fnid + u32::try_from(other_entryid_offset).unwrap() + }) + .collect(), + }), + (Some(mut self_type_data), Some(other_type_data)) => { + self_type_data.inverted_function_signature_index.extend( + other_type_data + .inverted_function_signature_index + .iter() + .copied() + .map(|fnid| { + assert!( + other.function_data[usize::try_from(fnid).unwrap()] + .is_some(), + ); + // this is valid because we call `self.push()` once, exactly, for every entry, + // even if we're just pushing a tombstone + fnid + u32::try_from(other_entryid_offset).unwrap() + }), + ); + Some(self_type_data) + } + (None, None) => None, + }; + self.type_data[self_pathid] = new_type_data; + self_pathid + } + }; + map_other_pathid_to_self_pathid.push(self_pathid); + } else { + // if this gets used, we want it to crash + // this should be impossible as a valid index, since some of the + // memory must be used for stuff other than the list + map_other_pathid_to_self_pathid.push(!0); + } + } + for other_entryid in 0..other.names.len() { + if skips.contains(&other_entryid) { + // we push tombstone entries to keep the IDs lined up + self.push(String::new(), None, None, String::new(), None, None, None); + } else { + self.push( + other.names[other_entryid].clone(), + other.path_data[other_entryid].clone(), + other.entry_data[other_entryid].as_ref().map(|other_entry_data| EntryData { + parent: other_entry_data + .parent + .map(|parent| map_other_pathid_to_self_pathid[parent]) + .clone(), + module_path: other_entry_data + .module_path + .map(|path| map_other_pathid_to_self_pathid[path]) + .clone(), + exact_module_path: other_entry_data + .exact_module_path + .map(|exact_path| map_other_pathid_to_self_pathid[exact_path]) + .clone(), + krate: map_other_pathid_to_self_pathid[other_entry_data.krate], + ..other_entry_data.clone() + }), + other.descs[other_entryid].clone(), + other.function_data[other_entryid].as_ref().map(|function_data| FunctionData { + function_signature: { + let (mut func, _offset) = + IndexItemFunctionType::read_from_string_without_param_names( + function_data.function_signature.as_bytes(), + ); + fn map_fn_sig_item( + map_other_pathid_to_self_pathid: &mut Vec, + ty: &mut RenderType, + ) { + match ty.id { + None => {} + Some(RenderTypeId::Index(generic)) if generic < 0 => {} + Some(RenderTypeId::Index(id)) => { + let id = usize::try_from(id).unwrap(); + let id = map_other_pathid_to_self_pathid[id]; + assert!(id != !0); + ty.id = + Some(RenderTypeId::Index(isize::try_from(id).unwrap())); + } + _ => unreachable!(), + } + if let Some(generics) = &mut ty.generics { + for generic in generics { + map_fn_sig_item(map_other_pathid_to_self_pathid, generic); + } + } + if let Some(bindings) = &mut ty.bindings { + for (param, constraints) in bindings { + *param = match *param { + param @ RenderTypeId::Index(generic) if generic < 0 => { + param + } + RenderTypeId::Index(id) => { + let id = usize::try_from(id).unwrap(); + let id = map_other_pathid_to_self_pathid[id]; + assert!(id != !0); + RenderTypeId::Index(isize::try_from(id).unwrap()) + } + _ => unreachable!(), + }; + for constraint in constraints { + map_fn_sig_item( + map_other_pathid_to_self_pathid, + constraint, + ); + } + } + } + } + for input in &mut func.inputs { + map_fn_sig_item(&mut map_other_pathid_to_self_pathid, input); + } + for output in &mut func.output { + map_fn_sig_item(&mut map_other_pathid_to_self_pathid, output); + } + for clause in &mut func.where_clause { + for entry in clause { + map_fn_sig_item(&mut map_other_pathid_to_self_pathid, entry); + } + } + let mut result = + String::with_capacity(function_data.function_signature.len()); + func.write_to_string_without_param_names(&mut result); + result + }, + param_names: function_data.param_names.clone(), + }), + other.type_data[other_entryid].as_ref().map(|type_data| TypeData { + inverted_function_signature_index: type_data + .inverted_function_signature_index + .iter() + .map(|&fnid| { + assert!( + other.function_data[usize::try_from(fnid).unwrap()].is_some(), + ); + // this is valid because we call `self.push()` once, exactly, for every entry, + // even if we're just pushing a tombstone + fnid + u32::try_from(other_entryid_offset).unwrap() + }) + .collect(), + search_unbox: type_data.search_unbox, + }), + other.alias_pointers[other_entryid] + .map(|alias_pointer| alias_pointer + other_entryid_offset), + ); + } + } + for (i, other_generic_inverted_index) in other.generic_inverted_index.iter().enumerate() { + let invert = other_generic_inverted_index + .iter() + .copied() + .map(|fnid| fnid + u32::try_from(other_entryid_offset).unwrap()); + match self.generic_inverted_index.get_mut(i) { + Some(self_generic_inverted_index) => { + self_generic_inverted_index.extend(invert); + } + None => { + self.generic_inverted_index.push(invert.collect()); + } + } + } + self + } + + pub(crate) fn sort(self) -> SerializedSearchIndex { + let mut idlist: Vec = (0..self.names.len()).collect(); + // nameless entries are tombstones, and will be removed after sorting + // sort shorter names first, so that we can present them in order out of search.js + idlist.sort_by_key(|&id| { + ( + self.names[id].is_empty(), + self.names[id].len(), + &self.names[id], + self.entry_data[id].as_ref().map_or("", |entry| self.names[entry.krate].as_str()), + self.path_data[id].as_ref().map_or(&[][..], |entry| &entry.module_path[..]), + ) + }); + let map = FxHashMap::from_iter( + idlist.iter().enumerate().map(|(new_id, &old_id)| (old_id, new_id)), + ); + let mut new = SerializedSearchIndex::default(); + for &id in &idlist { + if self.names[id].is_empty() { + break; + } + new.push( + self.names[id].clone(), + self.path_data[id].clone(), + self.entry_data[id].as_ref().map( + |EntryData { + krate, + ty, + module_path, + exact_module_path, + parent, + deprecated, + associated_item_disambiguator, + }| EntryData { + krate: *map.get(krate).unwrap(), + ty: *ty, + module_path: module_path.and_then(|path_id| map.get(&path_id).copied()), + exact_module_path: exact_module_path + .and_then(|path_id| map.get(&path_id).copied()), + parent: parent.and_then(|path_id| map.get(&path_id).copied()), + deprecated: *deprecated, + associated_item_disambiguator: associated_item_disambiguator.clone(), + }, + ), + self.descs[id].clone(), + self.function_data[id].as_ref().map( + |FunctionData { function_signature, param_names }| FunctionData { + function_signature: { + let (mut func, _offset) = + IndexItemFunctionType::read_from_string_without_param_names( + function_signature.as_bytes(), + ); + fn map_fn_sig_item(map: &FxHashMap, ty: &mut RenderType) { + match ty.id { + None => {} + Some(RenderTypeId::Index(generic)) if generic < 0 => {} + Some(RenderTypeId::Index(id)) => { + let id = usize::try_from(id).unwrap(); + let id = *map.get(&id).unwrap(); + assert!(id != !0); + ty.id = + Some(RenderTypeId::Index(isize::try_from(id).unwrap())); + } + _ => unreachable!(), + } + if let Some(generics) = &mut ty.generics { + for generic in generics { + map_fn_sig_item(map, generic); + } + } + if let Some(bindings) = &mut ty.bindings { + for (param, constraints) in bindings { + *param = match *param { + param @ RenderTypeId::Index(generic) if generic < 0 => { + param + } + RenderTypeId::Index(id) => { + let id = usize::try_from(id).unwrap(); + let id = *map.get(&id).unwrap(); + assert!(id != !0); + RenderTypeId::Index(isize::try_from(id).unwrap()) + } + _ => unreachable!(), + }; + for constraint in constraints { + map_fn_sig_item(map, constraint); + } + } + } + } + for input in &mut func.inputs { + map_fn_sig_item(&map, input); + } + for output in &mut func.output { + map_fn_sig_item(&map, output); + } + for clause in &mut func.where_clause { + for entry in clause { + map_fn_sig_item(&map, entry); + } + } + let mut result = String::with_capacity(function_signature.len()); + func.write_to_string_without_param_names(&mut result); + result + }, + param_names: param_names.clone(), + }, + ), + self.type_data[id].as_ref().map( + |TypeData { search_unbox, inverted_function_signature_index }| { + let mut inverted_function_signature_index: Vec = + inverted_function_signature_index + .iter() + .copied() + .map(|id| { + u32::try_from(*map.get(&usize::try_from(id).unwrap()).unwrap()) + .unwrap() + }) + .collect(); + inverted_function_signature_index.sort(); + TypeData { search_unbox: *search_unbox, inverted_function_signature_index } + }, + ), + self.alias_pointers[id].and_then(|alias| map.get(&alias).copied()), + ); + } + new.generic_inverted_index = self + .generic_inverted_index + .into_iter() + .map(|old_list| { + let mut new_list: Vec = old_list + .into_iter() + .filter_map(|id| u32::try_from(*map.get(&usize::try_from(id).ok()?)?).ok()) + .collect(); + new_list.sort(); + new_list + }) + .collect(); + new + } + + pub(crate) fn write_to(self, doc_root: &Path, resource_suffix: &str) -> Result<(), Error> { + let SerializedSearchIndex { + names, + path_data, + entry_data, + descs, + function_data, + type_data, + alias_pointers, + generic_inverted_index, + crate_paths_index: _, + } = self; + let mut serialized_root = Vec::new(); + serialized_root.extend_from_slice(br#"rr_('{"normalizedName":{"I":""#); + let normalized_names = names + .iter() + .map(|name| { + if name.contains("_") { + name.replace("_", "").to_ascii_lowercase() + } else { + name.to_ascii_lowercase() + } + }) + .collect::>(); + let names_search_tree = stringdex_internals::tree::encode_search_tree_ukkonen( + normalized_names.iter().map(|name| name.as_bytes()), + ); + let dir_path = doc_root.join(format!("search.index/")); + let _ = std::fs::remove_dir_all(&dir_path); // if already missing, no problem + stringdex_internals::write_tree_to_disk( + &names_search_tree, + &dir_path, + &mut serialized_root, + ) + .map_err(|error| Error { + file: dir_path, + error: format!("failed to write name tree to disk: {error}"), + })?; + std::mem::drop(names_search_tree); + serialized_root.extend_from_slice(br#"","#); + serialized_root.extend_from_slice(&perform_write_strings( + doc_root, + "normalizedName", + normalized_names.into_iter(), + )?); + serialized_root.extend_from_slice(br#"},"crateNames":{"#); + let mut crates: Vec<&[u8]> = entry_data + .iter() + .filter_map(|entry_data| Some(names[entry_data.as_ref()?.krate].as_bytes())) + .collect(); + crates.sort(); + crates.dedup(); + serialized_root.extend_from_slice(&perform_write_strings( + doc_root, + "crateNames", + crates.into_iter(), + )?); + serialized_root.extend_from_slice(br#"},"name":{"#); + serialized_root.extend_from_slice(&perform_write_strings(doc_root, "name", names.iter())?); + serialized_root.extend_from_slice(br#"},"path":{"#); + serialized_root.extend_from_slice(&perform_write_serde(doc_root, "path", path_data)?); + serialized_root.extend_from_slice(br#"},"entry":{"#); + serialized_root.extend_from_slice(&perform_write_serde(doc_root, "entry", entry_data)?); + serialized_root.extend_from_slice(br#"},"desc":{"#); + serialized_root.extend_from_slice(&perform_write_strings( + doc_root, + "desc", + descs.into_iter(), + )?); + serialized_root.extend_from_slice(br#"},"function":{"#); + serialized_root.extend_from_slice(&perform_write_serde( + doc_root, + "function", + function_data, + )?); + serialized_root.extend_from_slice(br#"},"type":{"#); + serialized_root.extend_from_slice(&perform_write_serde(doc_root, "type", type_data)?); + serialized_root.extend_from_slice(br#"},"alias":{"#); + serialized_root.extend_from_slice(&perform_write_serde(doc_root, "alias", alias_pointers)?); + serialized_root.extend_from_slice(br#"},"generic_inverted_index":{"#); + serialized_root.extend_from_slice(&perform_write_bitmap( + doc_root, + "generic_inverted_index", + generic_inverted_index, + )?); + serialized_root.extend_from_slice(br#"}}')"#); + fn perform_write_strings( + doc_root: &Path, + dirname: &str, + mut column: impl Iterator + Clone> + ExactSizeIterator, + ) -> Result, Error> { + let dir_path = doc_root.join(format!("search.index/{dirname}")); + stringdex_internals::write_data_to_disk(&mut column, &dir_path).map_err(|error| Error { + file: dir_path, + error: format!("failed to write column to disk: {error}"), + }) + } + fn perform_write_serde( + doc_root: &Path, + dirname: &str, + column: Vec>, + ) -> Result, Error> { + perform_write_strings( + doc_root, + dirname, + column.into_iter().map(|value| { + if let Some(value) = value { + serde_json::to_vec(&value).unwrap() + } else { + Vec::new() + } + }), + ) + } + fn perform_write_bitmap( + doc_root: &Path, + dirname: &str, + column: Vec>, + ) -> Result, Error> { + perform_write_strings( + doc_root, + dirname, + column.into_iter().map(|value| { + if value.is_empty() { + Vec::new() + } else { + let mut buf = Vec::new(); + stringdex_internals::encode::write_bitmap_to_bytes(&value, &mut buf) + .unwrap(); + let mut serialized_result = Vec::new(); + stringdex_internals::encode::write_base64_to_bytes( + &buf, + &mut serialized_result, + ); + serialized_result + } + }), + ) + } + std::fs::write( + doc_root.join(format!("search.index/root{resource_suffix}.js")), + serialized_root, + ) + .map_err(|error| Error { + file: doc_root.join(format!("search.index/root{resource_suffix}.js")), + error: format!("failed to write root to disk: {error}"), + })?; + Ok(()) + } +} + +#[derive(Clone, Debug)] +struct EntryData { + krate: usize, + ty: ItemType, + module_path: Option, + exact_module_path: Option, + parent: Option, + deprecated: bool, + associated_item_disambiguator: Option, +} + +impl Serialize for EntryData { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut seq = serializer.serialize_seq(None)?; + seq.serialize_element(&self.krate)?; + seq.serialize_element(&self.ty)?; + seq.serialize_element(&self.module_path.map(|id| id + 1).unwrap_or(0))?; + seq.serialize_element(&self.exact_module_path.map(|id| id + 1).unwrap_or(0))?; + seq.serialize_element(&self.parent.map(|id| id + 1).unwrap_or(0))?; + seq.serialize_element(&if self.deprecated { 1 } else { 0 })?; + if let Some(disambig) = &self.associated_item_disambiguator { + seq.serialize_element(&disambig)?; + } + seq.end() + } +} + +impl<'de> Deserialize<'de> for EntryData { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct EntryDataVisitor; + impl<'de> de::Visitor<'de> for EntryDataVisitor { + type Value = EntryData; + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "path data") + } + fn visit_seq>(self, mut v: A) -> Result { + let krate: usize = + v.next_element()?.ok_or_else(|| A::Error::missing_field("krate"))?; + let ty: ItemType = + v.next_element()?.ok_or_else(|| A::Error::missing_field("ty"))?; + let module_path: SerializedOptional32 = + v.next_element()?.ok_or_else(|| A::Error::missing_field("module_path"))?; + let exact_module_path: SerializedOptional32 = v + .next_element()? + .ok_or_else(|| A::Error::missing_field("exact_module_path"))?; + let parent: SerializedOptional32 = + v.next_element()?.ok_or_else(|| A::Error::missing_field("parent"))?; + let deprecated: u32 = v.next_element()?.unwrap_or(0); + let associated_item_disambiguator: Option = v.next_element()?; + Ok(EntryData { + krate, + ty, + module_path: Option::::from(module_path).map(|path| path as usize), + exact_module_path: Option::::from(exact_module_path) + .map(|path| path as usize), + parent: Option::::from(parent).map(|path| path as usize), + deprecated: deprecated != 0, + associated_item_disambiguator, + }) + } + } + deserializer.deserialize_any(EntryDataVisitor) + } +} + +#[derive(Clone, Debug)] +struct PathData { + ty: ItemType, + module_path: Vec, + exact_module_path: Option>, +} + +impl Serialize for PathData { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut seq = serializer.serialize_seq(None)?; + seq.serialize_element(&self.ty)?; + seq.serialize_element(&if self.module_path.is_empty() { + String::new() + } else { + join_path_syms(&self.module_path) + })?; + if let Some(ref path) = self.exact_module_path { + seq.serialize_element(&if path.is_empty() { + String::new() + } else { + join_path_syms(path) + })?; + } + seq.end() + } +} + +impl<'de> Deserialize<'de> for PathData { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct PathDataVisitor; + impl<'de> de::Visitor<'de> for PathDataVisitor { + type Value = PathData; + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "path data") + } + fn visit_seq>(self, mut v: A) -> Result { + let ty: ItemType = + v.next_element()?.ok_or_else(|| A::Error::missing_field("ty"))?; + let module_path: String = + v.next_element()?.ok_or_else(|| A::Error::missing_field("module_path"))?; + let exact_module_path: Option = + v.next_element()?.and_then(SerializedOptionalString::into); + Ok(PathData { + ty, + module_path: if module_path.is_empty() { + vec![] + } else { + module_path.split("::").map(Symbol::intern).collect() + }, + exact_module_path: exact_module_path.map(|path| { + if path.is_empty() { + vec![] + } else { + path.split("::").map(Symbol::intern).collect() + } + }), + }) + } + } + deserializer.deserialize_any(PathDataVisitor) + } +} + +#[derive(Clone, Debug)] +struct TypeData { + search_unbox: bool, + inverted_function_signature_index: Vec, +} + +impl Serialize for TypeData { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + if self.search_unbox || !self.inverted_function_signature_index.is_empty() { + let mut seq = serializer.serialize_seq(None)?; + if !self.inverted_function_signature_index.is_empty() { + let mut buf = Vec::new(); + stringdex_internals::encode::write_bitmap_to_bytes( + &self.inverted_function_signature_index, + &mut buf, + ) + .unwrap(); + let mut serialized_result = Vec::new(); + stringdex_internals::encode::write_base64_to_bytes(&buf, &mut serialized_result); + seq.serialize_element(&String::from_utf8(serialized_result).unwrap())?; + } + if self.search_unbox { + seq.serialize_element(&1)?; + } + seq.end() + } else { + None::<()>.serialize(serializer) + } + } +} + +impl<'de> Deserialize<'de> for TypeData { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct TypeDataVisitor; + impl<'de> de::Visitor<'de> for TypeDataVisitor { + type Value = TypeData; + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "type data") + } + fn visit_none(self) -> Result { + Ok(TypeData { inverted_function_signature_index: vec![], search_unbox: false }) + } + fn visit_seq>(self, mut v: A) -> Result { + let inverted_function_signature_index: String = + v.next_element()?.unwrap_or(String::new()); + let search_unbox: u32 = v.next_element()?.unwrap_or(0); + let mut idx: Vec = Vec::new(); + stringdex_internals::decode::read_base64_from_bytes( + inverted_function_signature_index.as_bytes(), + &mut idx, + ) + .unwrap(); + let inverted_function_signature_index = RoaringBitmap::from_bytes(&idx) + .map(|(bitmap, _offset)| bitmap) + .unwrap_or_else(RoaringBitmap::default); + Ok(TypeData { + inverted_function_signature_index: inverted_function_signature_index.to_vec(), + search_unbox: search_unbox == 1, + }) + } + } + deserializer.deserialize_any(TypeDataVisitor) + } +} + +enum SerializedOptionalString { + None, + Some(String), +} + +impl From for Option { + fn from(me: SerializedOptionalString) -> Option { + match me { + SerializedOptionalString::Some(string) => Some(string), + SerializedOptionalString::None => None, + } + } +} + +impl Serialize for SerializedOptionalString { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match self { + SerializedOptionalString::Some(string) => string.serialize(serializer), + SerializedOptionalString::None => 0.serialize(serializer), + } + } +} +impl<'de> Deserialize<'de> for SerializedOptionalString { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct SerializedOptionalStringVisitor; + impl<'de> de::Visitor<'de> for SerializedOptionalStringVisitor { + type Value = SerializedOptionalString; + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "0 or string") + } + fn visit_u64(self, v: u64) -> Result { + if v != 0 { + return Err(E::missing_field("not 0")); + } + Ok(SerializedOptionalString::None) + } + fn visit_string(self, v: String) -> Result { + Ok(SerializedOptionalString::Some(v)) + } + fn visit_str(self, v: &str) -> Result { + Ok(SerializedOptionalString::Some(v.to_string())) + } + } + deserializer.deserialize_any(SerializedOptionalStringVisitor) + } +} + +enum SerializedOptional32 { + None, + Some(i32), +} + +impl From for Option { + fn from(me: SerializedOptional32) -> Option { + match me { + SerializedOptional32::Some(number) => Some(number), + SerializedOptional32::None => None, + } + } +} + +impl Serialize for SerializedOptional32 { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match self { + &SerializedOptional32::Some(number) if number < 0 => number.serialize(serializer), + &SerializedOptional32::Some(number) => (number + 1).serialize(serializer), + &SerializedOptional32::None => 0.serialize(serializer), + } + } +} +impl<'de> Deserialize<'de> for SerializedOptional32 { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct SerializedOptional32Visitor; + impl<'de> de::Visitor<'de> for SerializedOptional32Visitor { + type Value = SerializedOptional32; + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "integer") + } + fn visit_i64(self, v: i64) -> Result { + Ok(match v { + 0 => SerializedOptional32::None, + v if v < 0 => SerializedOptional32::Some(v as i32), + v => SerializedOptional32::Some(v as i32 - 1), + }) + } + fn visit_u64(self, v: u64) -> Result { + Ok(match v { + 0 => SerializedOptional32::None, + v => SerializedOptional32::Some(v as i32 - 1), + }) + } + } + deserializer.deserialize_any(SerializedOptional32Visitor) + } +} + +#[derive(Clone, Debug)] +pub struct FunctionData { + function_signature: String, + param_names: Vec, } -const DESC_INDEX_SHARD_LEN: usize = 128 * 1024; +impl Serialize for FunctionData { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut seq = serializer.serialize_seq(None)?; + seq.serialize_element(&self.function_signature)?; + seq.serialize_element(&self.param_names)?; + seq.end() + } +} + +impl<'de> Deserialize<'de> for FunctionData { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct FunctionDataVisitor; + impl<'de> de::Visitor<'de> for FunctionDataVisitor { + type Value = FunctionData; + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "fn data") + } + fn visit_seq>(self, mut v: A) -> Result { + let function_signature: String = v + .next_element()? + .ok_or_else(|| A::Error::missing_field("function_signature"))?; + let param_names: Vec = + v.next_element()?.ok_or_else(|| A::Error::missing_field("param_names"))?; + Ok(FunctionData { function_signature, param_names }) + } + } + deserializer.deserialize_any(FunctionDataVisitor) + } +} /// Builds the search index from the collected metadata pub(crate) fn build_index( krate: &clean::Crate, cache: &mut Cache, tcx: TyCtxt<'_>, -) -> SerializedSearchIndex { - // Maps from ID to position in the `crate_paths` array. - let mut itemid_to_pathid = FxHashMap::default(); - let mut primitives = FxHashMap::default(); - let mut associated_types = FxHashMap::default(); - - // item type, display path, re-exported internal path - let mut crate_paths: Vec<(ItemType, Vec, Option>, bool)> = vec![]; + doc_root: &Path, + resource_suffix: &str, +) -> Result { + let mut search_index = std::mem::take(&mut cache.search_index); // Attach all orphan items to the type's definition if the type // has since been learned. @@ -74,15 +1130,15 @@ pub(crate) fn build_index( { if let Some((fqp, _)) = cache.paths.get(&parent) { let desc = short_markdown_summary(&item.doc_value(), &item.link_names(cache)); - cache.search_index.push(IndexItem { + search_index.push(IndexItem { ty: item.type_(), defid: item.item_id.as_def_id(), name: item.name.unwrap(), - path: join_path_syms(&fqp[..fqp.len() - 1]), + module_path: fqp[..fqp.len() - 1].to_vec(), desc, parent: Some(parent), parent_idx: None, - exact_path: None, + exact_module_path: None, impl_id, search_type: get_function_type_for_search( item, @@ -97,85 +1153,297 @@ pub(crate) fn build_index( } } + // Sort search index items. This improves the compressibility of the search index. + search_index.sort_unstable_by(|k1, k2| { + // `sort_unstable_by_key` produces lifetime errors + // HACK(rustdoc): should not be sorting `CrateNum` or `DefIndex`, this will soon go away, too + let k1 = + (&k1.module_path, k1.name.as_str(), &k1.ty, k1.parent.map(|id| (id.index, id.krate))); + let k2 = + (&k2.module_path, k2.name.as_str(), &k2.ty, k2.parent.map(|id| (id.index, id.krate))); + Ord::cmp(&k1, &k2) + }); + + // Now, convert to an on-disk search index format + // + // if there's already a search index, load it into memory and add the new entries to it + // otherwise, do nothing + let mut serialized_index = SerializedSearchIndex::load(doc_root, resource_suffix)?; + + // The crate always goes first in this list + let crate_name = krate.name(tcx); let crate_doc = short_markdown_summary(&krate.module.doc_value(), &krate.module.link_names(cache)); + let crate_idx = { + let crate_path = (ItemType::ExternCrate, vec![crate_name]); + match serialized_index.crate_paths_index.entry(crate_path) { + Entry::Occupied(index) => { + let index = *index.get(); + serialized_index.descs[index] = crate_doc; + for type_data in serialized_index.type_data.iter_mut() { + if let Some(TypeData { inverted_function_signature_index, .. }) = type_data { + inverted_function_signature_index.retain(|fnid| { + serialized_index.entry_data[usize::try_from(*fnid).unwrap()] + .as_ref() + .unwrap() + .krate + != index + }); + } + } + for i in (index + 1)..serialized_index.entry_data.len() { + // if this crate has been built before, replace its stuff with new + if let Some(EntryData { krate, .. }) = serialized_index.entry_data[i] + && krate == index + { + serialized_index.entry_data[i] = None; + serialized_index.descs[i] = String::new(); + serialized_index.function_data[i] = None; + if serialized_index.path_data[i].is_none() { + serialized_index.names[i] = String::new(); + } + } + if let Some(alias_pointer) = serialized_index.alias_pointers[i] + && serialized_index.entry_data[alias_pointer].is_none() + { + serialized_index.alias_pointers[i] = None; + if serialized_index.path_data[i].is_none() + && serialized_index.entry_data[i].is_none() + { + serialized_index.names[i] = String::new(); + } + } + } + index + } + Entry::Vacant(slot) => { + let krate = serialized_index.names.len(); + slot.insert(krate); + serialized_index.push( + crate_name.as_str().to_string(), + Some(PathData { + ty: ItemType::ExternCrate, + module_path: vec![], + exact_module_path: None, + }), + Some(EntryData { + krate, + ty: ItemType::ExternCrate, + module_path: None, + exact_module_path: None, + parent: None, + deprecated: false, + associated_item_disambiguator: None, + }), + crate_doc, + None, + None, + None, + ); + krate + } + } + }; - #[derive(Eq, Ord, PartialEq, PartialOrd)] - struct SerSymbolAsStr(Symbol); + // First, populate associated item parents + let crate_items: Vec<&mut IndexItem> = search_index + .iter_mut() + .map(|item| { + item.parent_idx = item.parent.and_then(|defid| { + cache.paths.get(&defid).map(|&(ref fqp, ty)| { + let pathid = serialized_index.names.len(); + match serialized_index.crate_paths_index.entry((ty, fqp.clone())) { + Entry::Occupied(entry) => *entry.get(), + Entry::Vacant(entry) => { + entry.insert(pathid); + let (name, path) = fqp.split_last().unwrap(); + serialized_index.push_path( + name.as_str().to_string(), + PathData { + ty, + module_path: path.to_vec(), + exact_module_path: if let Some(exact_path) = + cache.exact_paths.get(&defid) + && let Some((name2, exact_path)) = exact_path.split_last() + && name == name2 + { + Some(exact_path.to_vec()) + } else { + None + }, + }, + ); + usize::try_from(pathid).unwrap() + } + } + }) + }); + + if let Some(defid) = item.defid + && item.parent_idx.is_none() + { + // If this is a re-export, retain the original path. + // Associated items don't use this. + // Their parent carries the exact fqp instead. + let exact_fqp = cache + .exact_paths + .get(&defid) + .or_else(|| cache.external_paths.get(&defid).map(|(fqp, _)| fqp)); + item.exact_module_path = exact_fqp.and_then(|fqp| { + // Re-exports only count if the name is exactly the same. + // This is a size optimization, since it means we only need + // to store the name once (and the path is re-used for everything + // exported from this same module). It's also likely to Do + // What I Mean, since if a re-export changes the name, it might + // also be a change in semantic meaning. + if fqp.last() != Some(&item.name) { + return None; + } + let path = + if item.ty == ItemType::Macro && tcx.has_attr(defid, sym::macro_export) { + // `#[macro_export]` always exports to the crate root. + vec![tcx.crate_name(defid.krate)] + } else { + if fqp.len() < 2 { + return None; + } + fqp[..fqp.len() - 1].to_vec() + }; + if path == item.module_path { + return None; + } + Some(path) + }); + } else if let Some(parent_idx) = item.parent_idx { + let i = usize::try_from(parent_idx).unwrap(); + item.module_path = + serialized_index.path_data[i].as_ref().unwrap().module_path.clone(); + item.exact_module_path = + serialized_index.path_data[i].as_ref().unwrap().exact_module_path.clone(); + } - impl Serialize for SerSymbolAsStr { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, + &mut *item + }) + .collect(); + + // Now, find anywhere that the same name is used for two different items + // these need a disambiguator hash for lints + let mut associated_item_duplicates = FxHashMap::<(usize, ItemType, Symbol), usize>::default(); + for item in crate_items.iter().map(|x| &*x) { + if item.impl_id.is_some() + && let Some(parent_idx) = item.parent_idx { - self.0.as_str().serialize(serializer) + let count = + associated_item_duplicates.entry((parent_idx, item.ty, item.name)).or_insert(0); + *count += 1; } } - type AliasMap = BTreeMap>; - // Aliases added through `#[doc(alias = "...")]`. Since a few items can have the same alias, - // we need the alias element to have an array of items. - let mut aliases: AliasMap = BTreeMap::new(); + // now populate the actual entries, type data, and function data + for item in crate_items { + assert_eq!( + item.parent.is_some(), + item.parent_idx.is_some(), + "`{}` is missing idx", + item.name + ); - // Sort search index items. This improves the compressibility of the search index. - cache.search_index.sort_unstable_by(|k1, k2| { - // `sort_unstable_by_key` produces lifetime errors - // HACK(rustdoc): should not be sorting `CrateNum` or `DefIndex`, this will soon go away, too - let k1 = (&k1.path, k1.name.as_str(), &k1.ty, k1.parent.map(|id| (id.index, id.krate))); - let k2 = (&k2.path, k2.name.as_str(), &k2.ty, k2.parent.map(|id| (id.index, id.krate))); - Ord::cmp(&k1, &k2) - }); + let module_path = Some(serialized_index.get_id_by_module_path(&item.module_path)); + let exact_module_path = item + .exact_module_path + .as_ref() + .map(|path| serialized_index.get_id_by_module_path(path)); + + let new_entry_id = serialized_index.push( + item.name.as_str().to_string(), + None, + Some(EntryData { + ty: item.ty, + parent: item.parent_idx, + module_path, + exact_module_path, + deprecated: item.deprecation.is_some(), + associated_item_disambiguator: if let Some(impl_id) = item.impl_id + && let Some(parent_idx) = item.parent_idx + && associated_item_duplicates + .get(&(parent_idx, item.ty, item.name)) + .copied() + .unwrap_or(0) + > 1 + { + Some(render::get_id_for_impl(tcx, ItemId::DefId(impl_id))) + } else { + None + }, + krate: crate_idx, + }), + item.desc.to_string(), + None, // filled in after all the types have been indexed + None, + None, + ); - // Set up alias indexes. - for (i, item) in cache.search_index.iter().enumerate() { + // Aliases + // ------- for alias in &item.aliases[..] { - aliases.entry(SerSymbolAsStr(*alias)).or_default().push(i); + serialized_index.push_alias(alias.as_str().to_string(), new_entry_id); } - } - - // Reduce `DefId` in paths into smaller sequential numbers, - // and prune the paths that do not appear in the index. - let mut lastpath = ""; - let mut lastpathid = 0isize; - // First, on function signatures - let mut search_index = std::mem::take(&mut cache.search_index); - for item in search_index.iter_mut() { - fn insert_into_map( - map: &mut FxHashMap, - itemid: F, - lastpathid: &mut isize, - crate_paths: &mut Vec<(ItemType, Vec, Option>, bool)>, - item_type: ItemType, + // Function signature reverse index + // -------------------------------- + fn insert_into_map( + ty: ItemType, path: &[Symbol], exact_path: Option<&[Symbol]>, search_unbox: bool, + serialized_index: &mut SerializedSearchIndex, + used_in_function_signature: &mut BTreeSet, ) -> RenderTypeId { - match map.entry(itemid) { - Entry::Occupied(entry) => RenderTypeId::Index(*entry.get()), + let pathid = serialized_index.names.len(); + let pathid = match serialized_index.crate_paths_index.entry((ty, path.to_vec())) { + Entry::Occupied(entry) => { + let id = *entry.get(); + if serialized_index.type_data[id].as_mut().is_none() { + serialized_index.type_data[id] = Some(TypeData { + search_unbox, + inverted_function_signature_index: Vec::new(), + }); + } else if search_unbox { + serialized_index.type_data[id].as_mut().unwrap().search_unbox = true; + } + id + } Entry::Vacant(entry) => { - let pathid = *lastpathid; entry.insert(pathid); - *lastpathid += 1; - crate_paths.push(( - item_type, - path.to_vec(), - exact_path.map(|path| path.to_vec()), - search_unbox, - )); - RenderTypeId::Index(pathid) + let (name, path) = path.split_last().unwrap(); + serialized_index.push_type( + name.to_string(), + PathData { + ty, + module_path: path.to_vec(), + exact_module_path: if let Some(exact_path) = exact_path + && let Some((name2, exact_path)) = exact_path.split_last() + && name == name2 + { + Some(exact_path.to_vec()) + } else { + None + }, + }, + TypeData { search_unbox, inverted_function_signature_index: Vec::new() }, + ); + pathid } - } + }; + used_in_function_signature.insert(isize::try_from(pathid).unwrap()); + RenderTypeId::Index(isize::try_from(pathid).unwrap()) } fn convert_render_type_id( id: RenderTypeId, cache: &mut Cache, - itemid_to_pathid: &mut FxHashMap, - primitives: &mut FxHashMap, - associated_types: &mut FxHashMap, - lastpathid: &mut isize, - crate_paths: &mut Vec<(ItemType, Vec, Option>, bool)>, + serialized_index: &mut SerializedSearchIndex, + used_in_function_signature: &mut BTreeSet, tcx: TyCtxt<'_>, ) -> Option { use crate::clean::PrimitiveType; @@ -192,39 +1460,55 @@ pub(crate) fn build_index( }; match id { RenderTypeId::Mut => Some(insert_into_map( - primitives, - kw::Mut, - lastpathid, - crate_paths, ItemType::Keyword, &[kw::Mut], None, search_unbox, + serialized_index, + used_in_function_signature, )), RenderTypeId::DefId(defid) => { if let Some(&(ref fqp, item_type)) = paths.get(&defid).or_else(|| external_paths.get(&defid)) { - let exact_fqp = exact_paths - .get(&defid) - .or_else(|| external_paths.get(&defid).map(|(fqp, _)| fqp)) - // Re-exports only count if the name is exactly the same. - // This is a size optimization, since it means we only need - // to store the name once (and the path is re-used for everything - // exported from this same module). It's also likely to Do - // What I Mean, since if a re-export changes the name, it might - // also be a change in semantic meaning. - .filter(|this_fqp| this_fqp.last() == fqp.last()); - Some(insert_into_map( - itemid_to_pathid, - ItemId::DefId(defid), - lastpathid, - crate_paths, - item_type, - fqp, - exact_fqp.map(|x| &x[..]).filter(|exact_fqp| exact_fqp != fqp), - search_unbox, - )) + if tcx.lang_items().fn_mut_trait() == Some(defid) + || tcx.lang_items().fn_once_trait() == Some(defid) + || tcx.lang_items().fn_trait() == Some(defid) + { + let name = *fqp.last().unwrap(); + // Make absolutely sure we use this single, correct path, + // because search.js needs to match. If we don't do this, + // there are three different paths that these traits may + // appear to come from. + Some(insert_into_map( + item_type, + &[sym::core, sym::ops, name], + Some(&[sym::core, sym::ops, name]), + search_unbox, + serialized_index, + used_in_function_signature, + )) + } else { + let exact_fqp = exact_paths + .get(&defid) + .or_else(|| external_paths.get(&defid).map(|(fqp, _)| fqp)) + .map(|v| &v[..]) + // Re-exports only count if the name is exactly the same. + // This is a size optimization, since it means we only need + // to store the name once (and the path is re-used for everything + // exported from this same module). It's also likely to Do + // What I Mean, since if a re-export changes the name, it might + // also be a change in semantic meaning. + .filter(|this_fqp| this_fqp.last() == fqp.last()); + Some(insert_into_map( + item_type, + fqp, + exact_fqp, + search_unbox, + serialized_index, + used_in_function_signature, + )) + } } else { None } @@ -232,26 +1516,25 @@ pub(crate) fn build_index( RenderTypeId::Primitive(primitive) => { let sym = primitive.as_sym(); Some(insert_into_map( - primitives, - sym, - lastpathid, - crate_paths, ItemType::Primitive, &[sym], None, search_unbox, + serialized_index, + used_in_function_signature, )) } - RenderTypeId::Index(_) => Some(id), + RenderTypeId::Index(index) => { + used_in_function_signature.insert(index); + Some(id) + } RenderTypeId::AssociatedType(sym) => Some(insert_into_map( - associated_types, - sym, - lastpathid, - crate_paths, ItemType::AssocType, &[sym], None, search_unbox, + serialized_index, + used_in_function_signature, )), } } @@ -259,11 +1542,8 @@ pub(crate) fn build_index( fn convert_render_type( ty: &mut RenderType, cache: &mut Cache, - itemid_to_pathid: &mut FxHashMap, - primitives: &mut FxHashMap, - associated_types: &mut FxHashMap, - lastpathid: &mut isize, - crate_paths: &mut Vec<(ItemType, Vec, Option>, bool)>, + serialized_index: &mut SerializedSearchIndex, + used_in_function_signature: &mut BTreeSet, tcx: TyCtxt<'_>, ) { if let Some(generics) = &mut ty.generics { @@ -271,11 +1551,8 @@ pub(crate) fn build_index( convert_render_type( item, cache, - itemid_to_pathid, - primitives, - associated_types, - lastpathid, - crate_paths, + serialized_index, + used_in_function_signature, tcx, ); } @@ -285,11 +1562,8 @@ pub(crate) fn build_index( let converted_associated_type = convert_render_type_id( *associated_type, cache, - itemid_to_pathid, - primitives, - associated_types, - lastpathid, - crate_paths, + serialized_index, + used_in_function_signature, tcx, ); let Some(converted_associated_type) = converted_associated_type else { @@ -300,11 +1574,8 @@ pub(crate) fn build_index( convert_render_type( constraint, cache, - itemid_to_pathid, - primitives, - associated_types, - lastpathid, - crate_paths, + serialized_index, + used_in_function_signature, tcx, ); } @@ -318,24 +1589,74 @@ pub(crate) fn build_index( ty.id = convert_render_type_id( id, cache, - itemid_to_pathid, - primitives, - associated_types, - lastpathid, - crate_paths, + serialized_index, + used_in_function_signature, tcx, ); + use crate::clean::PrimitiveType; + // These cases are added to the inverted index, but not actually included + // in the signature. There's a matching set of cases in the + // `unifyFunctionTypeIsMatchCandidate` function, for the slow path. + match id { + // typeNameIdOfArrayOrSlice + RenderTypeId::Primitive(PrimitiveType::Array | PrimitiveType::Slice) => { + insert_into_map( + ItemType::Primitive, + &[Symbol::intern("[]")], + None, + false, + serialized_index, + used_in_function_signature, + ); + } + RenderTypeId::Primitive(PrimitiveType::Tuple | PrimitiveType::Unit) => { + // typeNameIdOfArrayOrSlice + insert_into_map( + ItemType::Primitive, + &[Symbol::intern("()")], + None, + false, + serialized_index, + used_in_function_signature, + ); + } + // typeNameIdOfHof + RenderTypeId::Primitive(PrimitiveType::Fn) => { + insert_into_map( + ItemType::Primitive, + &[Symbol::intern("->")], + None, + false, + serialized_index, + used_in_function_signature, + ); + } + RenderTypeId::DefId(did) + if tcx.lang_items().fn_mut_trait() == Some(did) + || tcx.lang_items().fn_once_trait() == Some(did) + || tcx.lang_items().fn_trait() == Some(did) => + { + insert_into_map( + ItemType::Primitive, + &[Symbol::intern("->")], + None, + false, + serialized_index, + used_in_function_signature, + ); + } + // not special + _ => {} + } } if let Some(search_type) = &mut item.search_type { + let mut used_in_function_signature = BTreeSet::new(); for item in &mut search_type.inputs { convert_render_type( item, cache, - &mut itemid_to_pathid, - &mut primitives, - &mut associated_types, - &mut lastpathid, - &mut crate_paths, + &mut serialized_index, + &mut used_in_function_signature, tcx, ); } @@ -343,11 +1664,8 @@ pub(crate) fn build_index( convert_render_type( item, cache, - &mut itemid_to_pathid, - &mut primitives, - &mut associated_types, - &mut lastpathid, - &mut crate_paths, + &mut serialized_index, + &mut used_in_function_signature, tcx, ); } @@ -356,464 +1674,45 @@ pub(crate) fn build_index( convert_render_type( trait_, cache, - &mut itemid_to_pathid, - &mut primitives, - &mut associated_types, - &mut lastpathid, - &mut crate_paths, + &mut serialized_index, + &mut used_in_function_signature, tcx, ); } } - } - } - - let Cache { ref paths, ref exact_paths, ref external_paths, .. } = *cache; - - // Then, on parent modules - let crate_items: Vec<&IndexItem> = search_index - .iter_mut() - .map(|item| { - item.parent_idx = - item.parent.and_then(|defid| match itemid_to_pathid.entry(ItemId::DefId(defid)) { - Entry::Occupied(entry) => Some(*entry.get()), - Entry::Vacant(entry) => { - let pathid = lastpathid; - entry.insert(pathid); - lastpathid += 1; - - if let Some(&(ref fqp, short)) = paths.get(&defid) { - let exact_fqp = exact_paths - .get(&defid) - .or_else(|| external_paths.get(&defid).map(|(fqp, _)| fqp)) - .filter(|exact_fqp| { - exact_fqp.last() == Some(&item.name) && *exact_fqp != fqp - }); - crate_paths.push(( - short, - fqp.clone(), - exact_fqp.cloned(), - utils::has_doc_flag(tcx, defid, sym::search_unbox), - )); - Some(pathid) - } else { - None - } - } - }); - - if let Some(defid) = item.defid - && item.parent_idx.is_none() - { - // If this is a re-export, retain the original path. - // Associated items don't use this. - // Their parent carries the exact fqp instead. - let exact_fqp = exact_paths - .get(&defid) - .or_else(|| external_paths.get(&defid).map(|(fqp, _)| fqp)); - item.exact_path = exact_fqp.and_then(|fqp| { - // Re-exports only count if the name is exactly the same. - // This is a size optimization, since it means we only need - // to store the name once (and the path is re-used for everything - // exported from this same module). It's also likely to Do - // What I Mean, since if a re-export changes the name, it might - // also be a change in semantic meaning. - if fqp.last() != Some(&item.name) { - return None; - } - let path = - if item.ty == ItemType::Macro && tcx.has_attr(defid, sym::macro_export) { - // `#[macro_export]` always exports to the crate root. - tcx.crate_name(defid.krate).to_string() - } else { - if fqp.len() < 2 { - return None; - } - join_path_syms(&fqp[..fqp.len() - 1]) - }; - if path == item.path { - return None; - } - Some(path) - }); - } else if let Some(parent_idx) = item.parent_idx { - let i = >::try_into(parent_idx).unwrap(); - item.path = { - let p = &crate_paths[i].1; - join_path_syms(&p[..p.len() - 1]) - }; - item.exact_path = - crate_paths[i].2.as_ref().map(|xp| join_path_syms(&xp[..xp.len() - 1])); - } - - // Omit the parent path if it is same to that of the prior item. - if lastpath == item.path { - item.path.clear(); - } else { - lastpath = &item.path; - } - - &*item - }) - .collect(); - - // Find associated items that need disambiguators - let mut associated_item_duplicates = FxHashMap::<(isize, ItemType, Symbol), usize>::default(); - - for &item in &crate_items { - if item.impl_id.is_some() - && let Some(parent_idx) = item.parent_idx - { - let count = - associated_item_duplicates.entry((parent_idx, item.ty, item.name)).or_insert(0); - *count += 1; - } - } - - let associated_item_disambiguators = crate_items - .iter() - .enumerate() - .filter_map(|(index, item)| { - let impl_id = ItemId::DefId(item.impl_id?); - let parent_idx = item.parent_idx?; - let count = *associated_item_duplicates.get(&(parent_idx, item.ty, item.name))?; - if count > 1 { Some((index, render::get_id_for_impl(tcx, impl_id))) } else { None } - }) - .collect::>(); - - struct CrateData<'a> { - items: Vec<&'a IndexItem>, - paths: Vec<(ItemType, Vec, Option>, bool)>, - // The String is alias name and the vec is the list of the elements with this alias. - // - // To be noted: the `usize` elements are indexes to `items`. - aliases: &'a AliasMap, - // Used when a type has more than one impl with an associated item with the same name. - associated_item_disambiguators: &'a Vec<(usize, String)>, - // A list of shard lengths encoded as vlqhex. See the comment in write_vlqhex_to_string - // for information on the format. - desc_index: String, - // A list of items with no description. This is eventually turned into a bitmap. - empty_desc: Vec, - } - - struct Paths { - ty: ItemType, - name: Symbol, - path: Option, - exact_path: Option, - search_unbox: bool, - } - - impl Serialize for Paths { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - let mut seq = serializer.serialize_seq(None)?; - seq.serialize_element(&self.ty)?; - seq.serialize_element(self.name.as_str())?; - if let Some(ref path) = self.path { - seq.serialize_element(path)?; - } - if let Some(ref path) = self.exact_path { - assert!(self.path.is_some()); - seq.serialize_element(path)?; - } - if self.search_unbox { - if self.path.is_none() { - seq.serialize_element(&None::)?; - } - if self.exact_path.is_none() { - seq.serialize_element(&None::)?; - } - seq.serialize_element(&1)?; - } - seq.end() - } - } - - impl Serialize for CrateData<'_> { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - let mut extra_paths = FxHashMap::default(); - // We need to keep the order of insertion, hence why we use an `IndexMap`. Then we will - // insert these "extra paths" (which are paths of items from external crates) into the - // `full_paths` list at the end. - let mut revert_extra_paths = FxIndexMap::default(); - let mut mod_paths = FxHashMap::default(); - for (index, item) in self.items.iter().enumerate() { - if item.path.is_empty() { - continue; - } - mod_paths.insert(&item.path, index); - } - let mut paths = Vec::with_capacity(self.paths.len()); - for &(ty, ref path, ref exact, search_unbox) in &self.paths { - if path.len() < 2 { - paths.push(Paths { - ty, - name: path[0], - path: None, - exact_path: None, - search_unbox, - }); - continue; - } - let full_path = join_path_syms(&path[..path.len() - 1]); - let full_exact_path = exact - .as_ref() - .filter(|exact| exact.last() == path.last() && exact.len() >= 2) - .map(|exact| join_path_syms(&exact[..exact.len() - 1])); - let exact_path = extra_paths.len() + self.items.len(); - let exact_path = full_exact_path.as_ref().map(|full_exact_path| match extra_paths - .entry(full_exact_path.clone()) - { - Entry::Occupied(entry) => *entry.get(), - Entry::Vacant(entry) => { - if let Some(index) = mod_paths.get(&full_exact_path) { - return *index; - } - entry.insert(exact_path); - if !revert_extra_paths.contains_key(&exact_path) { - revert_extra_paths.insert(exact_path, full_exact_path.clone()); - } - exact_path - } - }); - if let Some(index) = mod_paths.get(&full_path) { - paths.push(Paths { - ty, - name: *path.last().unwrap(), - path: Some(*index), - exact_path, - search_unbox, - }); - continue; - } - // It means it comes from an external crate so the item and its path will be - // stored into another array. - // - // `index` is put after the last `mod_paths` - let index = extra_paths.len() + self.items.len(); - match extra_paths.entry(full_path.clone()) { - Entry::Occupied(entry) => { - paths.push(Paths { - ty, - name: *path.last().unwrap(), - path: Some(*entry.get()), - exact_path, - search_unbox, - }); - } - Entry::Vacant(entry) => { - entry.insert(index); - if !revert_extra_paths.contains_key(&index) { - revert_extra_paths.insert(index, full_path); - } - paths.push(Paths { - ty, - name: *path.last().unwrap(), - path: Some(index), - exact_path, - search_unbox, - }); - } - } - } - - // Direct exports use adjacent arrays for the current crate's items, - // but re-exported exact paths don't. - let mut re_exports = Vec::new(); - for (item_index, item) in self.items.iter().enumerate() { - if let Some(exact_path) = item.exact_path.as_ref() { - if let Some(path_index) = mod_paths.get(&exact_path) { - re_exports.push((item_index, *path_index)); - } else { - let path_index = extra_paths.len() + self.items.len(); - let path_index = match extra_paths.entry(exact_path.clone()) { - Entry::Occupied(entry) => *entry.get(), - Entry::Vacant(entry) => { - entry.insert(path_index); - if !revert_extra_paths.contains_key(&path_index) { - revert_extra_paths.insert(path_index, exact_path.clone()); - } - path_index - } - }; - re_exports.push((item_index, path_index)); - } - } - } - - let mut names = Vec::with_capacity(self.items.len()); - let mut types = String::with_capacity(self.items.len()); - let mut full_paths = Vec::with_capacity(self.items.len()); - let mut parents = String::with_capacity(self.items.len()); - let mut parents_backref_queue = VecDeque::new(); - let mut functions = String::with_capacity(self.items.len()); - let mut deprecated = Vec::with_capacity(self.items.len()); - - let mut type_backref_queue = VecDeque::new(); - - let mut last_name = None; - for (index, item) in self.items.iter().enumerate() { - let n = item.ty as u8; - let c = char::from(n + b'A'); - assert!(c <= 'z', "item types must fit within ASCII printables"); - types.push(c); - - assert_eq!( - item.parent.is_some(), - item.parent_idx.is_some(), - "`{}` is missing idx", - item.name - ); - assert!( - parents_backref_queue.len() <= 16, - "the string encoding only supports 16 slots of lookback" - ); - let parent: i32 = item.parent_idx.map(|x| x + 1).unwrap_or(0).try_into().unwrap(); - if let Some(idx) = parents_backref_queue.iter().position(|p: &i32| *p == parent) { - parents.push( - char::try_from('0' as u32 + u32::try_from(idx).unwrap()) - .expect("last possible value is '?'"), - ); - } else if parent == 0 { - write_vlqhex_to_string(parent, &mut parents); - } else { - parents_backref_queue.push_front(parent); - write_vlqhex_to_string(parent, &mut parents); - if parents_backref_queue.len() > 16 { - parents_backref_queue.pop_back(); - } - } - - if Some(item.name.as_str()) == last_name { - names.push(""); + serialized_index.function_data[new_entry_id] = Some(FunctionData { + function_signature: { + let mut function_signature = String::new(); + search_type.write_to_string_without_param_names(&mut function_signature); + function_signature + }, + param_names: search_type + .param_names + .iter() + .map(|sym| sym.map(|sym| sym.to_string()).unwrap_or(String::new())) + .collect::>(), + }); + for index in used_in_function_signature { + if index >= 0 { + assert!(serialized_index.path_data[index as usize].is_some()); + serialized_index.type_data[index as usize] + .as_mut() + .unwrap() + .inverted_function_signature_index + .push(new_entry_id as u32); } else { - names.push(item.name.as_str()); - last_name = Some(item.name.as_str()); - } - - if !item.path.is_empty() { - full_paths.push((index, &item.path)); - } - - match &item.search_type { - Some(ty) => ty.write_to_string(&mut functions, &mut type_backref_queue), - None => functions.push('`'), - } - - if item.deprecation.is_some() { - // bitmasks always use 1-indexing for items, with 0 as the crate itself - deprecated.push(u32::try_from(index + 1).unwrap()); - } - } - - for (index, path) in &revert_extra_paths { - full_paths.push((*index, path)); - } - - let param_names: Vec<(usize, String)> = { - let mut prev = Vec::new(); - let mut result = Vec::new(); - for (index, item) in self.items.iter().enumerate() { - if let Some(ty) = &item.search_type - && let my = ty - .param_names - .iter() - .filter_map(|sym| sym.map(|sym| sym.to_string())) - .collect::>() - && my != prev - { - result.push((index, my.join(","))); - prev = my; + let generic_id = usize::try_from(-index).unwrap() - 1; + for _ in serialized_index.generic_inverted_index.len()..=generic_id { + serialized_index.generic_inverted_index.push(Vec::new()); } + serialized_index.generic_inverted_index[generic_id] + .push(u32::try_from(new_entry_id).unwrap()); } - result - }; - - let has_aliases = !self.aliases.is_empty(); - let mut crate_data = - serializer.serialize_struct("CrateData", if has_aliases { 13 } else { 12 })?; - crate_data.serialize_field("t", &types)?; - crate_data.serialize_field("n", &names)?; - crate_data.serialize_field("q", &full_paths)?; - crate_data.serialize_field("i", &parents)?; - crate_data.serialize_field("f", &functions)?; - crate_data.serialize_field("D", &self.desc_index)?; - crate_data.serialize_field("p", &paths)?; - crate_data.serialize_field("r", &re_exports)?; - crate_data.serialize_field("b", &self.associated_item_disambiguators)?; - crate_data.serialize_field("c", &bitmap_to_string(&deprecated))?; - crate_data.serialize_field("e", &bitmap_to_string(&self.empty_desc))?; - crate_data.serialize_field("P", ¶m_names)?; - if has_aliases { - crate_data.serialize_field("a", &self.aliases)?; } - crate_data.end() } } - let (empty_desc, desc) = { - let mut empty_desc = Vec::new(); - let mut result = Vec::new(); - let mut set = String::new(); - let mut len: usize = 0; - let mut item_index: u32 = 0; - for desc in std::iter::once(&crate_doc).chain(crate_items.iter().map(|item| &item.desc)) { - if desc.is_empty() { - empty_desc.push(item_index); - item_index += 1; - continue; - } - if set.len() >= DESC_INDEX_SHARD_LEN { - result.push((len, std::mem::take(&mut set))); - len = 0; - } else if len != 0 { - set.push('\n'); - } - set.push_str(desc); - len += 1; - item_index += 1; - } - result.push((len, std::mem::take(&mut set))); - (empty_desc, result) - }; - - let desc_index = { - let mut desc_index = String::with_capacity(desc.len() * 4); - for &(len, _) in desc.iter() { - write_vlqhex_to_string(len.try_into().unwrap(), &mut desc_index); - } - desc_index - }; - - assert_eq!( - crate_items.len() + 1, - desc.iter().map(|(len, _)| *len).sum::() + empty_desc.len() - ); - - // The index, which is actually used to search, is JSON - // It uses `JSON.parse(..)` to actually load, since JSON - // parses faster than the full JavaScript syntax. - let crate_name = krate.name(tcx); - let data = CrateData { - items: crate_items, - paths: crate_paths, - aliases: &aliases, - associated_item_disambiguators: &associated_item_disambiguators, - desc_index, - empty_desc, - }; - let index = OrderedJson::array_unsorted([ - OrderedJson::serialize(crate_name.as_str()).unwrap(), - OrderedJson::serialize(data).unwrap(), - ]); - SerializedSearchIndex { index, desc } + Ok(serialized_index.sort()) } pub(crate) fn get_function_type_for_search( diff --git a/src/librustdoc/html/render/search_index/encode.rs b/src/librustdoc/html/render/search_index/encode.rs index de2f54558ff81..7b0d483b78f13 100644 --- a/src/librustdoc/html/render/search_index/encode.rs +++ b/src/librustdoc/html/render/search_index/encode.rs @@ -1,6 +1,4 @@ -use base64::prelude::*; - -pub(crate) fn write_vlqhex_to_string(n: i32, string: &mut String) { +pub(crate) fn write_signed_vlqhex_to_string(n: i32, string: &mut String) { let (sign, magnitude): (bool, u32) = if n >= 0 { (false, n.try_into().unwrap()) } else { (true, (-n).try_into().unwrap()) }; // zig-zag encoding @@ -37,206 +35,18 @@ pub(crate) fn write_vlqhex_to_string(n: i32, string: &mut String) { } } -// Used during bitmap encoding -enum Container { - /// number of ones, bits - Bits(Box<[u64; 1024]>), - /// list of entries - Array(Vec), - /// list of (start, len-1) - Run(Vec<(u16, u16)>), -} -impl Container { - fn popcount(&self) -> u32 { - match self { - Container::Bits(bits) => bits.iter().copied().map(|x| x.count_ones()).sum(), - Container::Array(array) => { - array.len().try_into().expect("array can't be bigger than 2**32") - } - Container::Run(runs) => { - runs.iter().copied().map(|(_, lenm1)| u32::from(lenm1) + 1).sum() - } - } - } - fn push(&mut self, value: u16) { - match self { - Container::Bits(bits) => bits[value as usize >> 6] |= 1 << (value & 0x3F), - Container::Array(array) => { - array.push(value); - if array.len() >= 4096 { - let array = std::mem::take(array); - *self = Container::Bits(Box::new([0; 1024])); - for value in array { - self.push(value); - } - } - } - Container::Run(runs) => { - if let Some(r) = runs.last_mut() - && r.0 + r.1 + 1 == value - { - r.1 += 1; - } else { - runs.push((value, 0)); - } - } - } - } - fn try_make_run(&mut self) -> bool { - match self { - Container::Bits(bits) => { - let mut r: u64 = 0; - for (i, chunk) in bits.iter().copied().enumerate() { - let next_chunk = - i.checked_add(1).and_then(|i| bits.get(i)).copied().unwrap_or(0); - r += !chunk & u64::from((chunk << 1).count_ones()); - r += !next_chunk & u64::from((chunk >> 63).count_ones()); - } - if (2 + 4 * r) >= 8192 { - return false; - } - let bits = std::mem::replace(bits, Box::new([0; 1024])); - *self = Container::Run(Vec::new()); - for (i, bits) in bits.iter().copied().enumerate() { - if bits == 0 { - continue; - } - for j in 0..64 { - let value = (u16::try_from(i).unwrap() << 6) | j; - if bits & (1 << j) != 0 { - self.push(value); - } - } - } - true - } - Container::Array(array) if array.len() <= 5 => false, - Container::Array(array) => { - let mut r = 0; - let mut prev = None; - for value in array.iter().copied() { - if value.checked_sub(1) != prev { - r += 1; - } - prev = Some(value); - } - if 2 + 4 * r >= 2 * array.len() + 2 { - return false; - } - let array = std::mem::take(array); - *self = Container::Run(Vec::new()); - for value in array { - self.push(value); - } - true - } - Container::Run(_) => true, - } - } -} - -// checked against roaring-rs in -// https://gitlab.com/notriddle/roaring-test -pub(crate) fn write_bitmap_to_bytes( - domain: &[u32], - mut out: impl std::io::Write, -) -> std::io::Result<()> { - // https://arxiv.org/pdf/1603.06549.pdf - let mut keys = Vec::::new(); - let mut containers = Vec::::new(); - let mut key: u16; - let mut domain_iter = domain.iter().copied().peekable(); - let mut has_run = false; - while let Some(entry) = domain_iter.next() { - key = (entry >> 16).try_into().expect("shifted off the top 16 bits, so it should fit"); - let value: u16 = (entry & 0x00_00_FF_FF).try_into().expect("AND 16 bits, so it should fit"); - let mut container = Container::Array(vec![value]); - while let Some(entry) = domain_iter.peek().copied() { - let entry_key: u16 = - (entry >> 16).try_into().expect("shifted off the top 16 bits, so it should fit"); - if entry_key != key { - break; - } - domain_iter.next().expect("peeking just succeeded"); - container - .push((entry & 0x00_00_FF_FF).try_into().expect("AND 16 bits, so it should fit")); - } - keys.push(key); - has_run = container.try_make_run() || has_run; - containers.push(container); - } - // https://github.com/RoaringBitmap/RoaringFormatSpec - const SERIAL_COOKIE_NO_RUNCONTAINER: u32 = 12346; - const SERIAL_COOKIE: u32 = 12347; - const NO_OFFSET_THRESHOLD: u32 = 4; - let size: u32 = containers.len().try_into().unwrap(); - let start_offset = if has_run { - out.write_all(&u32::to_le_bytes(SERIAL_COOKIE | ((size - 1) << 16)))?; - for set in containers.chunks(8) { - let mut b = 0; - for (i, container) in set.iter().enumerate() { - if matches!(container, &Container::Run(..)) { - b |= 1 << i; - } - } - out.write_all(&[b])?; - } - if size < NO_OFFSET_THRESHOLD { - 4 + 4 * size + size.div_ceil(8) - } else { - 4 + 8 * size + size.div_ceil(8) - } - } else { - out.write_all(&u32::to_le_bytes(SERIAL_COOKIE_NO_RUNCONTAINER))?; - out.write_all(&u32::to_le_bytes(containers.len().try_into().unwrap()))?; - 4 + 4 + 4 * size + 4 * size - }; - for (&key, container) in keys.iter().zip(&containers) { - // descriptive header - let key: u32 = key.into(); - let count: u32 = container.popcount() - 1; - out.write_all(&u32::to_le_bytes((count << 16) | key))?; - } - if !has_run || size >= NO_OFFSET_THRESHOLD { - // offset header - let mut starting_offset = start_offset; - for container in &containers { - out.write_all(&u32::to_le_bytes(starting_offset))?; - starting_offset += match container { - Container::Bits(_) => 8192u32, - Container::Array(array) => u32::try_from(array.len()).unwrap() * 2, - Container::Run(runs) => 2 + u32::try_from(runs.len()).unwrap() * 4, - }; - } - } - for container in &containers { - match container { - Container::Bits(bits) => { - for chunk in bits.iter() { - out.write_all(&u64::to_le_bytes(*chunk))?; - } - } - Container::Array(array) => { - for value in array.iter() { - out.write_all(&u16::to_le_bytes(*value))?; - } - } - Container::Run(runs) => { - out.write_all(&u16::to_le_bytes(runs.len().try_into().unwrap()))?; - for (start, lenm1) in runs.iter().copied() { - out.write_all(&u16::to_le_bytes(start))?; - out.write_all(&u16::to_le_bytes(lenm1))?; - } - } - } - } - Ok(()) -} - -pub(crate) fn bitmap_to_string(domain: &[u32]) -> String { - let mut buf = Vec::new(); - let mut strbuf = String::new(); - write_bitmap_to_bytes(domain, &mut buf).unwrap(); - BASE64_STANDARD.encode_string(&buf, &mut strbuf); - strbuf +pub fn read_signed_vlqhex_from_string(string: &[u8]) -> Option<(i32, usize)> { + let mut n = 0i32; + let mut i = 0; + while let Some(&c) = string.get(i) { + i += 1; + n = (n << 4) | i32::from(c & 0xF); + if c >= 96 { + // zig-zag encoding + let (sign, magnitude) = (n & 1, n >> 1); + let value = if sign == 0 { 1 } else { -1 } * magnitude; + return Some((value, i)); + } + } + None } diff --git a/src/librustdoc/html/render/write_shared.rs b/src/librustdoc/html/render/write_shared.rs index 1f691392b1716..e37a5246a7684 100644 --- a/src/librustdoc/html/render/write_shared.rs +++ b/src/librustdoc/html/render/write_shared.rs @@ -65,17 +65,17 @@ pub(crate) fn write_shared( // Write shared runs within a flock; disable thread dispatching of IO temporarily. let _lock = try_err!(flock::Lock::new(&lock_file, true, true, true), &lock_file); - let SerializedSearchIndex { index, desc } = build_index(krate, &mut cx.shared.cache, tcx); - write_search_desc(cx, krate, &desc)?; // does not need to be merged + let search_index = + build_index(krate, &mut cx.shared.cache, tcx, &cx.dst, &cx.shared.resource_suffix)?; let crate_name = krate.name(cx.tcx()); let crate_name = crate_name.as_str(); // rand let crate_name_json = OrderedJson::serialize(crate_name).unwrap(); // "rand" let external_crates = hack_get_external_crate_names(&cx.dst, &cx.shared.resource_suffix)?; let info = CrateInfo { - version: CrateInfoVersion::V1, + version: CrateInfoVersion::V2, src_files_js: SourcesPart::get(cx, &crate_name_json)?, - search_index_js: SearchIndexPart::get(index, &cx.shared.resource_suffix)?, + search_index, all_crates: AllCratesPart::get(crate_name_json.clone(), &cx.shared.resource_suffix)?, crates_index: CratesIndexPart::get(crate_name, &external_crates)?, trait_impl: TraitAliasPart::get(cx, &crate_name_json)?, @@ -141,7 +141,7 @@ pub(crate) fn write_not_crate_specific( resource_suffix: &str, include_sources: bool, ) -> Result<(), Error> { - write_rendered_cross_crate_info(crates, dst, opt, include_sources)?; + write_rendered_cross_crate_info(crates, dst, opt, include_sources, resource_suffix)?; write_static_files(dst, opt, style_files, css_file_extension, resource_suffix)?; Ok(()) } @@ -151,13 +151,18 @@ fn write_rendered_cross_crate_info( dst: &Path, opt: &RenderOptions, include_sources: bool, + resource_suffix: &str, ) -> Result<(), Error> { let m = &opt.should_merge; if opt.should_emit_crate() { if include_sources { write_rendered_cci::(SourcesPart::blank, dst, crates, m)?; } - write_rendered_cci::(SearchIndexPart::blank, dst, crates, m)?; + crates + .iter() + .fold(SerializedSearchIndex::default(), |a, b| a.union(&b.search_index)) + .sort() + .write_to(dst, resource_suffix)?; write_rendered_cci::(AllCratesPart::blank, dst, crates, m)?; } write_rendered_cci::(TraitAliasPart::blank, dst, crates, m)?; @@ -215,38 +220,12 @@ fn write_static_files( Ok(()) } -/// Write the search description shards to disk -fn write_search_desc( - cx: &mut Context<'_>, - krate: &Crate, - search_desc: &[(usize, String)], -) -> Result<(), Error> { - let crate_name = krate.name(cx.tcx()).to_string(); - let encoded_crate_name = OrderedJson::serialize(&crate_name).unwrap(); - let path = PathBuf::from_iter([&cx.dst, Path::new("search.desc"), Path::new(&crate_name)]); - if path.exists() { - try_err!(fs::remove_dir_all(&path), &path); - } - for (i, (_, part)) in search_desc.iter().enumerate() { - let filename = static_files::suffix_path( - &format!("{crate_name}-desc-{i}-.js"), - &cx.shared.resource_suffix, - ); - let path = path.join(filename); - let part = OrderedJson::serialize(part).unwrap(); - let part = format!("searchState.loadedDescShard({encoded_crate_name}, {i}, {part})"); - create_parents(&path)?; - try_err!(fs::write(&path, part), &path); - } - Ok(()) -} - /// Contains pre-rendered contents to insert into the CCI template #[derive(Serialize, Deserialize, Clone, Debug)] pub(crate) struct CrateInfo { version: CrateInfoVersion, src_files_js: PartsAndLocations, - search_index_js: PartsAndLocations, + search_index: SerializedSearchIndex, all_crates: PartsAndLocations, crates_index: PartsAndLocations, trait_impl: PartsAndLocations, @@ -277,7 +256,7 @@ impl CrateInfo { /// to provide better diagnostics about including an invalid file. #[derive(Serialize, Deserialize, Clone, Debug)] enum CrateInfoVersion { - V1, + V2, } /// Paths (relative to the doc root) and their pre-merge contents @@ -331,36 +310,6 @@ trait CciPart: Sized + fmt::Display + DeserializeOwned + 'static { fn from_crate_info(crate_info: &CrateInfo) -> &PartsAndLocations; } -#[derive(Serialize, Deserialize, Clone, Default, Debug)] -struct SearchIndex; -type SearchIndexPart = Part; -impl CciPart for SearchIndexPart { - type FileFormat = sorted_template::Js; - fn from_crate_info(crate_info: &CrateInfo) -> &PartsAndLocations { - &crate_info.search_index_js - } -} - -impl SearchIndexPart { - fn blank() -> SortedTemplate<::FileFormat> { - SortedTemplate::from_before_after( - r"var searchIndex = new Map(JSON.parse('[", - r"]')); -if (typeof exports !== 'undefined') exports.searchIndex = searchIndex; -else if (window.initSearch) window.initSearch(searchIndex);", - ) - } - - fn get( - search_index: OrderedJson, - resource_suffix: &str, - ) -> Result, Error> { - let path = suffix_path("search-index.js", resource_suffix); - let search_index = EscapedJson::from(search_index); - Ok(PartsAndLocations::with(path, search_index)) - } -} - #[derive(Serialize, Deserialize, Clone, Default, Debug)] struct AllCrates; type AllCratesPart = Part; @@ -426,6 +375,7 @@ impl CratesIndexPart { fn blank(cx: &Context<'_>) -> SortedTemplate<::FileFormat> { let page = layout::Page { title: "Index of crates", + short_title: "Crates", css_class: "mod sys", root_path: "./", static_root_path: cx.shared.static_root_path.as_deref(), diff --git a/src/librustdoc/html/render/write_shared/tests.rs b/src/librustdoc/html/render/write_shared/tests.rs index 6f185e85345bc..1989a1f87aae6 100644 --- a/src/librustdoc/html/render/write_shared/tests.rs +++ b/src/librustdoc/html/render/write_shared/tests.rs @@ -29,14 +29,6 @@ fn sources_template() { assert_eq!(but_last_line(&template.to_string()), r#"createSrcSidebar('["u","v"]');"#); } -#[test] -fn sources_parts() { - let parts = - SearchIndexPart::get(OrderedJson::serialize(["foo", "bar"]).unwrap(), "suffix").unwrap(); - assert_eq!(&parts.parts[0].0, Path::new("search-indexsuffix.js")); - assert_eq!(&parts.parts[0].1.to_string(), r#"["foo","bar"]"#); -} - #[test] fn all_crates_template() { let mut template = AllCratesPart::blank(); @@ -54,31 +46,6 @@ fn all_crates_parts() { assert_eq!(&parts.parts[0].1.to_string(), r#""crate""#); } -#[test] -fn search_index_template() { - let mut template = SearchIndexPart::blank(); - assert_eq!( - but_last_line(&template.to_string()), - r"var searchIndex = new Map(JSON.parse('[]')); -if (typeof exports !== 'undefined') exports.searchIndex = searchIndex; -else if (window.initSearch) window.initSearch(searchIndex);" - ); - template.append(EscapedJson::from(OrderedJson::serialize([1, 2]).unwrap()).to_string()); - assert_eq!( - but_last_line(&template.to_string()), - r"var searchIndex = new Map(JSON.parse('[[1,2]]')); -if (typeof exports !== 'undefined') exports.searchIndex = searchIndex; -else if (window.initSearch) window.initSearch(searchIndex);" - ); - template.append(EscapedJson::from(OrderedJson::serialize([4, 3]).unwrap()).to_string()); - assert_eq!( - but_last_line(&template.to_string()), - r"var searchIndex = new Map(JSON.parse('[[1,2],[4,3]]')); -if (typeof exports !== 'undefined') exports.searchIndex = searchIndex; -else if (window.initSearch) window.initSearch(searchIndex);" - ); -} - #[test] fn crates_index_part() { let external_crates = ["bar".to_string(), "baz".to_string()]; diff --git a/src/librustdoc/html/sources.rs b/src/librustdoc/html/sources.rs index c34b31542697d..9c5518a780e44 100644 --- a/src/librustdoc/html/sources.rs +++ b/src/librustdoc/html/sources.rs @@ -230,6 +230,7 @@ impl SourceCollector<'_, '_> { ); let page = layout::Page { title: &title, + short_title: &src_fname.to_string_lossy(), css_class: "src", root_path: &root_path, static_root_path: shared.static_root_path.as_deref(), diff --git a/src/librustdoc/html/static/css/rustdoc.css b/src/librustdoc/html/static/css/rustdoc.css index 99b3da8b2cd47..003ac07087568 100644 --- a/src/librustdoc/html/static/css/rustdoc.css +++ b/src/librustdoc/html/static/css/rustdoc.css @@ -258,6 +258,17 @@ h1, h2, h3, h4 { padding-bottom: 6px; margin-bottom: 15px; } +.search-results-main-heading { + grid-template-areas: + "main-heading-breadcrumbs main-heading-placeholder" + "main-heading-breadcrumbs main-heading-toolbar " + "main-heading-h1 main-heading-toolbar "; +} +.search-results-main-heading nav.sub { + grid-area: main-heading-h1; + align-items: end; + margin: 4px 0 8px 0; +} .rustdoc-breadcrumbs { grid-area: main-heading-breadcrumbs; line-height: 1.25; @@ -265,6 +276,16 @@ h1, h2, h3, h4 { position: relative; z-index: 1; } +.search-switcher { + grid-area: main-heading-breadcrumbs; + line-height: 1.5; + display: flex; + color: var(--main-color); + align-items: baseline; + white-space: nowrap; + padding-top: 8px; + min-height: 34px; +} .rustdoc-breadcrumbs a { padding: 5px 0 7px; } @@ -305,7 +326,7 @@ h4.code-header { #crate-search, h1, h2, h3, h4, h5, h6, .sidebar, -.mobile-topbar, +rustdoc-topbar, .search-input, .search-results .result-name, .item-table dt > a, @@ -317,6 +338,7 @@ rustdoc-toolbar, summary.hideme, .scraped-example-list, .rustdoc-breadcrumbs, +.search-switcher, /* This selector is for the items listed in the "all items" page. */ ul.all-items { font-family: "Fira Sans", Arial, NanumBarunGothic, sans-serif; @@ -329,7 +351,7 @@ a.anchor, .rust a, .sidebar h2 a, .sidebar h3 a, -.mobile-topbar h2 a, +rustdoc-topbar h2 a, h1 a, .search-results a, .search-results li, @@ -616,7 +638,7 @@ img { color: var(--sidebar-resizer-active); } -.sidebar, .mobile-topbar, .sidebar-menu-toggle, +.sidebar, rustdoc-topbar, .sidebar-menu-toggle, #src-sidebar { background-color: var(--sidebar-background-color); } @@ -857,7 +879,7 @@ ul.block, .block li, .block ul { margin-bottom: 1rem; } -.mobile-topbar { +rustdoc-topbar { display: none; } @@ -1098,16 +1120,15 @@ div.where { nav.sub { flex-grow: 1; flex-flow: row nowrap; - margin: 4px 0 0 0; display: flex; - align-items: center; + align-items: start; + margin-top: 4px; } .search-form { position: relative; display: flex; height: 34px; flex-grow: 1; - margin-bottom: 4px; } .src nav.sub { margin: 0 0 -10px 0; @@ -1208,27 +1229,14 @@ table, margin-left: 0; } -.search-results-title { - margin-top: 0; - white-space: nowrap; - /* flex layout allows shrinking the -element "#crate-search") to be shrunk */ min-width: 0; + /* keep label text for switcher from moving down when this appears */ + margin-top: -1px; } #crate-search { padding: 0 23px 0 4px; @@ -1294,6 +1302,7 @@ so that we can apply CSS-filters to change the arrow color in themes */ flex-grow: 1; background-color: var(--button-background-color); color: var(--search-color); + max-width: 100%; } .search-input:focus { border-color: var(--search-input-focused-border-color); @@ -1970,7 +1979,7 @@ a.tooltip:hover::after { border-bottom: 1px solid var(--border-color); } -#settings-menu, #help-button, button#toggle-all-docs { +#search-button, .settings-menu, .help-menu, button#toggle-all-docs { margin-left: var(--button-left-margin); display: flex; line-height: 1.25; @@ -1985,6 +1994,8 @@ a.tooltip:hover::after { display: flex; margin-right: 4px; position: fixed; + margin-top: 25px; + left: 6px; height: 34px; width: 34px; } @@ -1994,60 +2005,87 @@ a.tooltip:hover::after { z-index: 1; } .src #sidebar-button { + margin-top: 0; + top: 8px; left: 8px; z-index: calc(var(--desktop-sidebar-z-index) + 1); + border-color: var(--border-color); } .hide-sidebar .src #sidebar-button { position: static; } -#settings-menu > a, #help-button > a, #sidebar-button > a, button#toggle-all-docs { +#search-button > a, +.settings-menu > a, +.help-menu > a, +#sidebar-button > a, +button#toggle-all-docs { display: flex; align-items: center; justify-content: center; flex-direction: column; } -#settings-menu > a, #help-button > a, button#toggle-all-docs { +#search-button > a, +.settings-menu > a, +.help-menu > a, +button#toggle-all-docs { border: 1px solid transparent; border-radius: var(--button-border-radius); color: var(--main-color); } -#settings-menu > a, #help-button > a, button#toggle-all-docs { +#search-button > a, .settings-menu > a, .help-menu > a, button#toggle-all-docs { width: 80px; border-radius: var(--toolbar-button-border-radius); } -#settings-menu > a, #help-button > a { +#search-button > a, .settings-menu > a, .help-menu > a { min-width: 0; } #sidebar-button > a { - background-color: var(--sidebar-background-color); + background-color: var(--button-background-color); width: 33px; } #sidebar-button > a:hover, #sidebar-button > a:focus-visible { background-color: var(--main-background-color); } -#settings-menu > a:hover, #settings-menu > a:focus-visible, -#help-button > a:hover, #help-button > a:focus-visible, +#search-button > a:hover, #search-button > a:focus-visible, +.settings-menu > a:hover, .settings-menu > a:focus-visible, +.help-menu > a:hover, #help-menu > a:focus-visible, +#sidebar-button > a:hover, #sidebar-button > a:focus-visible, +#copy-path:hover, #copy-path:focus-visible, button#toggle-all-docs:hover, button#toggle-all-docs:focus-visible { border-color: var(--settings-button-border-focus); text-decoration: none; } -#settings-menu > a::before { +#search-button > a::before { + /* Magnifying glass */ + content: url('data:image/svg+xml,\ + \ + Search\ + '); + width: 18px; + height: 18px; + filter: var(--settings-menu-filter); +} + +.settings-menu > a::before { /* Wheel */ content: url('data:image/svg+xml,\ - '); + \ + '); width: 18px; height: 18px; filter: var(--settings-menu-filter); @@ -2063,18 +2101,11 @@ button#toggle-all-docs::before { filter: var(--settings-menu-filter); } -button#toggle-all-docs.will-expand::before { - /* Custom arrow icon */ - content: url('data:image/svg+xml,\ - '); -} - -#help-button > a::before { +.help-menu > a::before { /* Question mark with circle */ content: url('data:image/svg+xml,\ - \ + \ ?'); width: 18px; @@ -2082,17 +2113,19 @@ button#toggle-all-docs.will-expand::before { filter: var(--settings-menu-filter); } +#search-button > a::before, button#toggle-all-docs::before, -#help-button > a::before, -#settings-menu > a::before { +.help-menu > a::before, +.settings-menu > a::before { filter: var(--settings-menu-filter); margin: 8px; } @media not (pointer: coarse) { + #search-button > a:hover::before, button#toggle-all-docs:hover::before, - #help-button > a:hover::before, - #settings-menu > a:hover::before { + .help-menu > a:hover::before, + .settings-menu > a:hover::before { filter: var(--settings-menu-hover-filter); } } @@ -2133,7 +2166,8 @@ rustdoc-toolbar span.label { margin-left: 10px; padding: 0; padding-left: 2px; - border: 0; + border: solid 1px transparent; + border-radius: var(--button-border-radius); font-size: 0; } #copy-path::before { @@ -2155,7 +2189,7 @@ rustdoc-toolbar span.label { transform: rotate(360deg); } } -#settings-menu.rotate > a img { +.settings-menu.rotate > a img { animation: rotating 2s linear infinite; } @@ -2406,8 +2440,8 @@ However, it's not needed with smaller screen width because the doc/code block is /* Media Queries */ -/* Make sure all the buttons line wrap at the same time */ @media (max-width: 850px) { + /* Make sure all the buttons line wrap at the same time */ #search-tabs .count { display: block; } @@ -2417,6 +2451,74 @@ However, it's not needed with smaller screen width because the doc/code block is .side-by-side > div { width: auto; } + + /* Text label takes up too much space at this size. */ + .main-heading { + grid-template-areas: + "main-heading-breadcrumbs main-heading-toolbar" + "main-heading-h1 main-heading-toolbar" + "main-heading-sub-heading main-heading-toolbar"; + } + .search-results-main-heading { + display: grid; + grid-template-areas: + "main-heading-breadcrumbs main-heading-toolbar" + "main-heading-breadcrumbs main-heading-toolbar" + "main-heading-h1 main-heading-toolbar"; + } + rustdoc-toolbar { + display: grid; + grid-template-areas: + "x settings help" + "search summary summary"; + } + .search-results-main-heading rustdoc-toolbar { + display: grid; + grid-template-areas: + "settings help" + "search search"; + } + .search-results-main-heading #toggle-all-docs { + display: none; + } + rustdoc-toolbar .settings-menu span.label, + rustdoc-toolbar .help-menu span.label + { + display: none; + } + rustdoc-toolbar .settings-menu { + grid-area: settings; + } + rustdoc-toolbar .help-menu { + grid-area: help; + } + rustdoc-toolbar .settings-menu { + grid-area: settings; + } + rustdoc-toolbar #search-button { + grid-area: search; + } + rustdoc-toolbar #toggle-all-docs { + grid-area: summary; + } + rustdoc-toolbar .settings-menu > a, + rustdoc-toolbar .help-menu > a { + border-radius: 2px; + text-align: center; + width: 40px; + } + rustdoc-toolbar .settings-menu > a:before, + rustdoc-toolbar .help-menu > a:before { + margin: 0 4px; + } + #settings.popover { + top: 16px; + --popover-arrow-offset: 58px; + } + #help.popover { + top: 16px; + --popover-arrow-offset: 16px; + } } /* @@ -2431,7 +2533,7 @@ in src-script.js and main.js /* When linking to an item with an `id` (for instance, by clicking a link in the sidebar, or visiting a URL with a fragment like `#method.new`, we don't want the item to be obscured - by the topbar. Anything with an `id` gets scroll-margin-top equal to .mobile-topbar's size. + by the topbar. Anything with an `id` gets scroll-margin-top equal to rustdoc-topbar's size. */ *[id] { scroll-margin-top: 45px; @@ -2447,18 +2549,45 @@ in src-script.js and main.js visibility: hidden; } - /* Text label takes up too much space at this size. */ - rustdoc-toolbar span.label { + + /* Pull settings and help up into the top bar. */ + .main-heading { + grid-template-areas: + "main-heading-breadcrumbs main-heading-breadcrumbs" + "main-heading-h1 main-heading-toolbar" + "main-heading-sub-heading main-heading-toolbar"; + } + .search-results-main-heading { + display: grid; + grid-template-areas: + "main-heading-breadcrumbs main-heading-placeholder" + "main-heading-breadcrumbs main-heading-toolbar" + "main-heading-h1 main-heading-toolbar"; + } + rustdoc-topbar span.label, + rustdoc-toolbar .settings-menu, + rustdoc-toolbar .help-menu + { display: none; } - #settings-menu > a, #help-button > a, button#toggle-all-docs { + rustdoc-topbar .settings-menu > a, + rustdoc-topbar .help-menu > a { width: 33px; + line-height: 0; + } + rustdoc-topbar .settings-menu > a:hover, + rustdoc-topbar .help-menu > a:hover { + border: none; + background: var(--main-background-color); + border-radius: 0; } #settings.popover { - --popover-arrow-offset: 86px; + top: 32px; + --popover-arrow-offset: 48px; } #help.popover { - --popover-arrow-offset: 48px; + top: 32px; + --popover-arrow-offset: 12px; } .rustdoc { @@ -2473,7 +2602,7 @@ in src-script.js and main.js } /* Hide the logo and item name from the sidebar. Those are displayed - in the mobile-topbar instead. */ + in the rustdoc-topbar instead. */ .sidebar .logo-container, .sidebar .location, .sidebar-resizer { @@ -2525,9 +2654,9 @@ in src-script.js and main.js left: 0; } - .mobile-topbar h2 { + rustdoc-topbar > h2 { padding-bottom: 0; - margin: auto 0.5em auto auto; + margin: auto; overflow: hidden; /* Rare exception to specifying font sizes in rem. Since the topbar height is specified in pixels, this also has to be specified in @@ -2536,32 +2665,34 @@ in src-script.js and main.js font-size: 24px; white-space: nowrap; text-overflow: ellipsis; + text-align: center; } - .mobile-topbar .logo-container > img { + rustdoc-topbar .logo-container > img { max-width: 35px; max-height: 35px; margin: 5px 0 5px 20px; } - .mobile-topbar { + rustdoc-topbar { display: flex; flex-direction: row; position: sticky; z-index: 10; - font-size: 2rem; height: 45px; width: 100%; left: 0; top: 0; } - .hide-sidebar .mobile-topbar { + .hide-sidebar rustdoc-topbar { display: none; } .sidebar-menu-toggle { - width: 45px; + /* prevent flexbox shrinking */ + width: 41px; + min-width: 41px; border: none; line-height: 0; } @@ -2584,6 +2715,9 @@ in src-script.js and main.js } /* sidebar button becomes topbar button */ + .hide-sidebar #sidebar-button { + margin-top: 0; + } #sidebar-button > a::before { content: url('data:image/svg+xml,\ @@ -3279,7 +3413,7 @@ Original by Dempfi (https://github.com/dempfi/ayu) border-bottom: 1px solid rgba(242, 151, 24, 0.3); } -:root[data-theme="ayu"] #settings-menu > a img, +:root[data-theme="ayu"] .settings-menu > a img, :root[data-theme="ayu"] #sidebar-button > a::before { filter: invert(100); } diff --git a/src/librustdoc/html/static/js/main.js b/src/librustdoc/html/static/js/main.js index 3c4af0dc612c4..0dae0e2a77587 100644 --- a/src/librustdoc/html/static/js/main.js +++ b/src/librustdoc/html/static/js/main.js @@ -54,23 +54,6 @@ function showMain() { window.rootPath = getVar("root-path"); window.currentCrate = getVar("current-crate"); -function setMobileTopbar() { - // FIXME: It would be nicer to generate this text content directly in HTML, - // but with the current code it's hard to get the right information in the right place. - const mobileTopbar = document.querySelector(".mobile-topbar"); - const locationTitle = document.querySelector(".sidebar h2.location"); - if (mobileTopbar) { - const mobileTitle = document.createElement("h2"); - mobileTitle.className = "location"; - if (hasClass(document.querySelector(".rustdoc"), "crate")) { - mobileTitle.innerHTML = `Crate ${window.currentCrate}`; - } else if (locationTitle) { - mobileTitle.innerHTML = locationTitle.innerHTML; - } - mobileTopbar.appendChild(mobileTitle); - } -} - /** * Gets the human-readable string for the virtual-key code of the * given KeyboardEvent, ev. @@ -84,6 +67,7 @@ function setMobileTopbar() { * So I guess you could say things are getting pretty interoperable. * * @param {KeyboardEvent} ev + * @returns {string} */ function getVirtualKey(ev) { if ("key" in ev && typeof ev.key !== "undefined") { @@ -98,18 +82,8 @@ function getVirtualKey(ev) { } const MAIN_ID = "main-content"; -const SETTINGS_BUTTON_ID = "settings-menu"; const ALTERNATIVE_DISPLAY_ID = "alternative-display"; const NOT_DISPLAYED_ID = "not-displayed"; -const HELP_BUTTON_ID = "help-button"; - -function getSettingsButton() { - return document.getElementById(SETTINGS_BUTTON_ID); -} - -function getHelpButton() { - return document.getElementById(HELP_BUTTON_ID); -} // Returns the current URL without any query parameter or hash. function getNakedUrl() { @@ -174,7 +148,7 @@ function getNotDisplayedElem() { * contains the displayed element (there can be only one at the same time!). So basically, we switch * elements between the two `
` elements. * - * @param {HTMLElement|null} elemToDisplay + * @param {Element|null} elemToDisplay */ function switchDisplayedElement(elemToDisplay) { const el = getAlternativeDisplayElem(); @@ -239,14 +213,14 @@ function preLoadCss(cssUrl) { document.head.append(script); } - const settingsButton = getSettingsButton(); - if (settingsButton) { - settingsButton.onclick = event => { + onEachLazy(document.querySelectorAll(".settings-menu"), settingsMenu => { + /** @param {MouseEvent} event */ + settingsMenu.onclick = event => { if (event.ctrlKey || event.altKey || event.metaKey) { return; } window.hideAllModals(false); - addClass(getSettingsButton(), "rotate"); + addClass(settingsMenu, "rotate"); event.preventDefault(); // Sending request for the CSS and the JS files at the same time so it will // hopefully be loaded when the JS will generate the settings content. @@ -268,15 +242,42 @@ function preLoadCss(cssUrl) { } }, 0); }; - } + }); window.searchState = { rustdocToolbar: document.querySelector("rustdoc-toolbar"), loadingText: "Loading search results...", - // This will always be an HTMLInputElement, but tsc can't see that - // @ts-expect-error - input: document.getElementsByClassName("search-input")[0], - outputElement: () => { + inputElement: () => { + let el = document.getElementsByClassName("search-input")[0]; + if (!el) { + const out = nonnull(nonnull(window.searchState.outputElement()).parentElement); + const hdr = document.createElement("div"); + hdr.className = "main-heading search-results-main-heading"; + const params = window.searchState.getQueryStringParams(); + const autofocusParam = params.search === "" ? "autofocus" : ""; + hdr.innerHTML = `
+ + +
`; + out.insertBefore(hdr, window.searchState.outputElement()); + el = document.getElementsByClassName("search-input")[0]; + } + if (el instanceof HTMLInputElement) { + return el; + } + return null; + }, + containerElement: () => { let el = document.getElementById("search"); if (!el) { el = document.createElement("section"); @@ -285,6 +286,19 @@ function preLoadCss(cssUrl) { } return el; }, + outputElement: () => { + const container = window.searchState.containerElement(); + if (!container) { + return null; + } + let el = container.querySelector(".search-out"); + if (!el) { + el = document.createElement("div"); + el.className = "search-out"; + container.appendChild(el); + } + return el; + }, title: document.title, titleBeforeSearch: document.title, timeout: null, @@ -303,25 +317,52 @@ function preLoadCss(cssUrl) { } }, isDisplayed: () => { - const outputElement = window.searchState.outputElement(); - return !!outputElement && - !!outputElement.parentElement && - outputElement.parentElement.id === ALTERNATIVE_DISPLAY_ID; + const container = window.searchState.containerElement(); + if (!container) { + return false; + } + return !!container.parentElement && container.parentElement.id === + ALTERNATIVE_DISPLAY_ID; }, // Sets the focus on the search bar at the top of the page focus: () => { - window.searchState.input && window.searchState.input.focus(); + const inputElement = window.searchState.inputElement(); + window.searchState.showResults(); + if (inputElement) { + inputElement.focus(); + // Avoid glitch if something focuses the search button after clicking. + requestAnimationFrame(() => inputElement.focus()); + } }, // Removes the focus from the search bar. defocus: () => { - window.searchState.input && window.searchState.input.blur(); + nonnull(window.searchState.inputElement()).blur(); }, - showResults: search => { - if (search === null || typeof search === "undefined") { - search = window.searchState.outputElement(); + toggle: () => { + if (window.searchState.isDisplayed()) { + window.searchState.defocus(); + window.searchState.hideResults(); + } else { + window.searchState.focus(); } - switchDisplayedElement(search); + }, + showResults: () => { document.title = window.searchState.title; + if (window.searchState.isDisplayed()) { + return; + } + const search = window.searchState.containerElement(); + switchDisplayedElement(search); + const btn = document.querySelector("#search-button a"); + if (browserSupportsHistoryApi() && btn instanceof HTMLAnchorElement && + window.searchState.getQueryStringParams().search === undefined + ) { + history.pushState(null, "", btn.href); + } + const btnLabel = document.querySelector("#search-button a span.label"); + if (btnLabel) { + btnLabel.innerHTML = "Exit"; + } }, removeQueryParameters: () => { // We change the document title. @@ -334,6 +375,10 @@ function preLoadCss(cssUrl) { switchDisplayedElement(null); // We also remove the query parameter from the URL. window.searchState.removeQueryParameters(); + const btnLabel = document.querySelector("#search-button a span.label"); + if (btnLabel) { + btnLabel.innerHTML = "Search"; + } }, getQueryStringParams: () => { /** @type {Object.} */ @@ -348,11 +393,11 @@ function preLoadCss(cssUrl) { return params; }, setup: () => { - const search_input = window.searchState.input; + let searchLoaded = false; + const search_input = window.searchState.inputElement(); if (!search_input) { return; } - let searchLoaded = false; // If you're browsing the nightly docs, the page might need to be refreshed for the // search to work because the hash of the JS scripts might have changed. function sendSearchForm() { @@ -363,21 +408,102 @@ function preLoadCss(cssUrl) { if (!searchLoaded) { searchLoaded = true; // @ts-expect-error - loadScript(getVar("static-root-path") + getVar("search-js"), sendSearchForm); - loadScript(resourcePath("search-index", ".js"), sendSearchForm); + window.rr_ = data => { + // @ts-expect-error + window.searchIndex = data; + }; + if (!window.StringdexOnload) { + window.StringdexOnload = []; + } + window.StringdexOnload.push(() => { + loadScript( + // @ts-expect-error + getVar("static-root-path") + getVar("search-js"), + sendSearchForm, + ); + }); + // @ts-expect-error + loadScript(getVar("static-root-path") + getVar("stringdex-js"), sendSearchForm); + loadScript(resourcePath("search.index/root", ".js"), sendSearchForm); } } search_input.addEventListener("focus", () => { - window.searchState.origPlaceholder = search_input.placeholder; - search_input.placeholder = "Type your search here."; loadSearch(); }); - if (search_input.value !== "") { - loadSearch(); + const btn = document.getElementById("search-button"); + if (btn) { + btn.onclick = event => { + if (event.ctrlKey || event.altKey || event.metaKey) { + return; + } + event.preventDefault(); + window.searchState.toggle(); + loadSearch(); + }; + } + + // Push and pop states are used to add search results to the browser + // history. + if (browserSupportsHistoryApi()) { + // Store the previous so we can revert back to it later. + const previousTitle = document.title; + + window.addEventListener("popstate", e => { + const params = window.searchState.getQueryStringParams(); + // Revert to the previous title manually since the History + // API ignores the title parameter. + document.title = previousTitle; + // Synchronize search bar with query string state and + // perform the search. This will empty the bar if there's + // nothing there, which lets you really go back to a + // previous state with nothing in the bar. + const inputElement = window.searchState.inputElement(); + if (params.search !== undefined && inputElement !== null) { + loadSearch(); + inputElement.value = params.search; + // Some browsers fire "onpopstate" for every page load + // (Chrome), while others fire the event only when actually + // popping a state (Firefox), which is why search() is + // called both here and at the end of the startSearch() + // function. + e.preventDefault(); + window.searchState.showResults(); + if (params.search === "") { + window.searchState.focus(); + } + } else { + // When browsing back from search results the main page + // visibility must be reset. + window.searchState.hideResults(); + } + }); } + // This is required in firefox to avoid this problem: Navigating to a search result + // with the keyboard, hitting enter, and then hitting back would take you back to + // the doc page, rather than the search that should overlay it. + // This was an interaction between the back-forward cache and our handlers + // that try to sync state between the URL and the search input. To work around it, + // do a small amount of re-init on page show. + window.onpageshow = () => { + const inputElement = window.searchState.inputElement(); + const qSearch = window.searchState.getQueryStringParams().search; + if (qSearch !== undefined && inputElement !== null) { + if (inputElement.value === "") { + inputElement.value = qSearch; + } + window.searchState.showResults(); + if (qSearch === "") { + loadSearch(); + window.searchState.focus(); + } + } else { + window.searchState.hideResults(); + } + }; + const params = window.searchState.getQueryStringParams(); if (params.search !== undefined) { window.searchState.setLoadingSearch(); @@ -386,13 +512,9 @@ function preLoadCss(cssUrl) { }, setLoadingSearch: () => { const search = window.searchState.outputElement(); - if (!search) { - return; - } - search.innerHTML = "<h3 class=\"search-loading\">" + - window.searchState.loadingText + - "</h3>"; - window.searchState.showResults(search); + nonnull(search).innerHTML = "<h3 class=\"search-loading\">" + + window.searchState.loadingText + "</h3>"; + window.searchState.showResults(); }, descShards: new Map(), loadDesc: async function({descShard, descIndex}) { @@ -1490,15 +1612,13 @@ function preLoadCss(cssUrl) { // @ts-expect-error function helpBlurHandler(event) { - // @ts-expect-error - if (!getHelpButton().contains(document.activeElement) && - // @ts-expect-error - !getHelpButton().contains(event.relatedTarget) && - // @ts-expect-error - !getSettingsButton().contains(document.activeElement) && - // @ts-expect-error - !getSettingsButton().contains(event.relatedTarget) - ) { + const isInPopover = onEachLazy( + document.querySelectorAll(".settings-menu, .help-menu"), + menu => { + return menu.contains(document.activeElement) || menu.contains(event.relatedTarget); + }, + ); + if (!isInPopover) { window.hidePopoverMenus(); } } @@ -1557,10 +1677,9 @@ function preLoadCss(cssUrl) { const container = document.createElement("div"); if (!isHelpPage) { - container.className = "popover"; + container.className = "popover content"; } container.id = "help"; - container.style.display = "none"; const side_by_side = document.createElement("div"); side_by_side.className = "side-by-side"; @@ -1576,17 +1695,16 @@ function preLoadCss(cssUrl) { help_section.appendChild(container); // @ts-expect-error document.getElementById("main-content").appendChild(help_section); - container.style.display = "block"; } else { - const help_button = getHelpButton(); - // @ts-expect-error - help_button.appendChild(container); - - container.onblur = helpBlurHandler; - // @ts-expect-error - help_button.onblur = helpBlurHandler; - // @ts-expect-error - help_button.children[0].onblur = helpBlurHandler; + onEachLazy(document.querySelectorAll(".help-menu"), menu => { + if (menu.offsetWidth !== 0) { + menu.appendChild(container); + container.onblur = helpBlurHandler; + menu.onblur = helpBlurHandler; + menu.children[0].onblur = helpBlurHandler; + return true; + } + }); } return container; @@ -1607,80 +1725,57 @@ function preLoadCss(cssUrl) { * Hide all the popover menus. */ window.hidePopoverMenus = () => { - onEachLazy(document.querySelectorAll("rustdoc-toolbar .popover"), elem => { + onEachLazy(document.querySelectorAll(".settings-menu .popover"), elem => { elem.style.display = "none"; }); - const button = getHelpButton(); - if (button) { - removeClass(button, "help-open"); - } + onEachLazy(document.querySelectorAll(".help-menu .popover"), elem => { + elem.parentElement.removeChild(elem); + }); }; - /** - * Returns the help menu element (not the button). - * - * @param {boolean} buildNeeded - If this argument is `false`, the help menu element won't be - * built if it doesn't exist. - * - * @return {HTMLElement} - */ - function getHelpMenu(buildNeeded) { - // @ts-expect-error - let menu = getHelpButton().querySelector(".popover"); - if (!menu && buildNeeded) { - menu = buildHelpMenu(); - } - // @ts-expect-error - return menu; - } - /** * Show the help popup menu. */ function showHelp() { + window.hideAllModals(false); // Prevent `blur` events from being dispatched as a result of closing // other modals. - const button = getHelpButton(); - addClass(button, "help-open"); - // @ts-expect-error - button.querySelector("a").focus(); - const menu = getHelpMenu(true); - if (menu.style.display === "none") { - // @ts-expect-error - window.hideAllModals(); - menu.style.display = ""; - } + onEachLazy(document.querySelectorAll(".help-menu a"), menu => { + if (menu.offsetWidth !== 0) { + menu.focus(); + return true; + } + }); + buildHelpMenu(); } - const helpLink = document.querySelector(`#${HELP_BUTTON_ID} > a`); if (isHelpPage) { buildHelpMenu(); - } else if (helpLink) { - helpLink.addEventListener("click", event => { - // By default, have help button open docs in a popover. - // If user clicks with a moderator, though, use default browser behavior, - // probably opening in a new window or tab. - if (!helpLink.contains(helpLink) || - // @ts-expect-error - event.ctrlKey || - // @ts-expect-error - event.altKey || - // @ts-expect-error - event.metaKey) { - return; - } - event.preventDefault(); - const menu = getHelpMenu(true); - const shouldShowHelp = menu.style.display === "none"; - if (shouldShowHelp) { - showHelp(); - } else { - window.hidePopoverMenus(); - } + } else { + onEachLazy(document.querySelectorAll(".help-menu > a"), helpLink => { + helpLink.addEventListener( + "click", + /** @param {MouseEvent} event */ + event => { + // By default, have help button open docs in a popover. + // If user clicks with a moderator, though, use default browser behavior, + // probably opening in a new window or tab. + if (event.ctrlKey || + event.altKey || + event.metaKey) { + return; + } + event.preventDefault(); + if (document.getElementById("help")) { + window.hidePopoverMenus(); + } else { + showHelp(); + } + }, + ); }); } - setMobileTopbar(); addSidebarItems(); addSidebarCrates(); onHashChange(null); @@ -1732,7 +1827,15 @@ function preLoadCss(cssUrl) { // On larger, "desktop-sized" viewports (though that includes many // tablets), it's fixed-position, appears in the left side margin, // and it can be activated by resizing the sidebar into nothing. - const sidebarButton = document.getElementById("sidebar-button"); + let sidebarButton = document.getElementById("sidebar-button"); + const body = document.querySelector(".main-heading"); + if (!sidebarButton && body) { + sidebarButton = document.createElement("div"); + sidebarButton.id = "sidebar-button"; + const path = `${window.rootPath}${window.currentCrate}/all.html`; + sidebarButton.innerHTML = `<a href="${path}" title="show sidebar"></a>`; + body.insertBefore(sidebarButton, body.firstChild); + } if (sidebarButton) { sidebarButton.addEventListener("click", e => { removeClass(document.documentElement, "hide-sidebar"); diff --git a/src/librustdoc/html/static/js/rustdoc.d.ts b/src/librustdoc/html/static/js/rustdoc.d.ts index a958976454781..9b75e553fa16a 100644 --- a/src/librustdoc/html/static/js/rustdoc.d.ts +++ b/src/librustdoc/html/static/js/rustdoc.d.ts @@ -2,6 +2,8 @@ // not put into the JavaScript we include as part of the documentation. It is used for // type checking. See README.md in this directory for more info. +import { RoaringBitmap } from "./stringdex"; + /* eslint-disable */ declare global { /** Search engine data used by main.js and search.js */ @@ -10,6 +12,17 @@ declare global { declare function nonnull(x: T|null, msg: string|undefined); /** Defined and documented in `storage.js` */ declare function nonundef(x: T|undefined, msg: string|undefined); + interface PromiseConstructor { + /** + * Polyfill + * @template T + */ + withResolvers: function(): { + "promise": Promise<T>, + "resolve": (function(T): void), + "reject": (function(any): void) + }; + } interface Window { /** Make the current theme easy to find */ currentTheme: HTMLLinkElement|null; @@ -95,29 +108,28 @@ declare namespace rustdoc { interface SearchState { rustdocToolbar: HTMLElement|null; loadingText: string; - input: HTMLInputElement|null; + inputElement: function(): HTMLInputElement|null; + containerElement: function(): Element|null; title: string; titleBeforeSearch: string; - timeout: number|null; + timeout: ReturnType<typeof setTimeout>|null; currentTab: number; - focusedByTab: [number|null, number|null, number|null]; + focusedByTab: [Element|null, Element|null, Element|null]; clearInputTimeout: function; - outputElement(): HTMLElement|null; - focus(); - defocus(); - // note: an optional param is not the same as - // a nullable/undef-able param. - showResults(elem?: HTMLElement|null); - removeQueryParameters(); - hideResults(); - getQueryStringParams(): Object.<any, string>; - origPlaceholder: string; + outputElement: function(): Element|null; + focus: function(); + defocus: function(); + toggle: function(); + showResults: function(); + removeQueryParameters: function(); + hideResults: function(); + getQueryStringParams: function(): Object.<any, string>; setup: function(); setLoadingSearch(); descShards: Map<string, SearchDescShard[]>; loadDesc: function({descShard: SearchDescShard, descIndex: number}): Promise<string|null>; - loadedDescShard(string, number, string); - isDisplayed(): boolean, + loadedDescShard: function(string, number, string); + isDisplayed: function(): boolean; } interface SearchDescShard { @@ -131,12 +143,13 @@ declare namespace rustdoc { * A single parsed "atom" in a search query. For example, * * std::fmt::Formatter, Write -> Result<()> - * ┏━━━━━━━━━━━━━━━━━━ ┌──── ┏━━━━━┅┅┅┅┄┄┄┄┄┄┄┄┄┄┄┄┄┄┐ - * ┃ │ ┗ QueryElement { ┊ - * ┃ │ name: Result ┊ - * ┃ │ generics: [ ┊ - * ┃ │ QueryElement ┘ - * ┃ │ name: () + * ┏━━━━━━━━━━━━━━━━━━ ┌──── ┏━━━━━┅┅┅┅┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┐ + * ┃ │ ┗ QueryElement { ┊ + * ┃ │ name: Result ┊ + * ┃ │ generics: [ ┊ + * ┃ │ QueryElement { ┘ + * ┃ │ name: () + * ┃ │ } * ┃ │ ] * ┃ │ } * ┃ └ QueryElement { @@ -156,14 +169,14 @@ declare namespace rustdoc { normalizedPathLast: string, generics: Array<QueryElement>, bindings: Map<number, Array<QueryElement>>, - typeFilter: number|null, + typeFilter: number, } /** * Same as QueryElement, but bindings and typeFilter support strings */ interface ParserQueryElement { - name: string|null, + name: string, id: number|null, fullPath: Array<string>, pathWithoutLast: Array<string>, @@ -172,7 +185,7 @@ declare namespace rustdoc { generics: Array<ParserQueryElement>, bindings: Map<string, Array<ParserQueryElement>>, bindingName: {name: string|null, generics: ParserQueryElement[]}|null, - typeFilter: number|string|null, + typeFilter: string|null, } /** @@ -215,35 +228,72 @@ declare namespace rustdoc { /** * An entry in the search index database. */ + interface EntryData { + krate: number, + ty: ItemType, + modulePath: number?, + exactModulePath: number?, + parent: number?, + deprecated: boolean, + associatedItemDisambiguator: string?, + } + + /** + * A path in the search index database + */ + interface PathData { + ty: ItemType, + modulePath: string, + exactModulePath: string?, + } + + /** + * A function signature in the search index database + */ + interface FunctionData { + functionSignature: FunctionSearchType|null, + paramNames: string[], + elemCount: number, + } + + /** + * A function signature in the search index database + */ + interface TypeData { + searchUnbox: boolean, + invertedFunctionSignatureIndex: RoaringBitmap, + } + + /** + * A search entry of some sort. + */ interface Row { - crate: string, - descShard: SearchDescShard, id: number, - // This is the name of the item. For doc aliases, if you want the name of the aliased - // item, take a look at `Row.original.name`. + crate: string, + ty: ItemType, name: string, normalizedName: string, - word: string, - paramNames: string[], - parent: ({ty: number, name: string, path: string, exactPath: string}|null|undefined), - path: string, - ty: number, - type: FunctionSearchType | null, - descIndex: number, - bitIndex: number, - implDisambiguator: String | null, - is_alias?: boolean, - original?: Row, + modulePath: string, + exactModulePath: string, + entry: EntryData?, + path: PathData?, + type: FunctionData?, + deprecated: boolean, + parent: { path: PathData, name: string}?, } + type ItemType = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | + 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | + 21 | 22 | 23 | 24 | 25 | 26; + /** * The viewmodel for the search engine results page. */ interface ResultsTable { - in_args: Array<ResultObject>, - returned: Array<ResultObject>, - others: Array<ResultObject>, - query: ParsedQuery, + in_args: AsyncGenerator<ResultObject>, + returned: AsyncGenerator<ResultObject>, + others: AsyncGenerator<ResultObject>, + query: ParsedQuery<rustdoc.ParserQueryElement>, } type Results = { max_dist?: number } & Map<number, ResultObject> @@ -252,25 +302,41 @@ declare namespace rustdoc { * An annotated `Row`, used in the viewmodel. */ interface ResultObject { - desc: string, + desc: Promise<string|null>, displayPath: string, fullPath: string, href: string, id: number, dist: number, path_dist: number, - name: string, - normalizedName: string, - word: string, index: number, - parent: (Object|undefined), - path: string, - ty: number, + parent: ({ + path: string, + exactPath: string, + name: string, + ty: number, + }|undefined), type?: FunctionSearchType, paramNames?: string[], displayTypeSignature: Promise<rustdoc.DisplayTypeSignature> | null, item: Row, - dontValidate?: boolean, + is_alias: boolean, + alias?: string, + } + + /** + * An annotated `Row`, used in the viewmodel. + */ + interface PlainResultObject { + id: number, + dist: number, + path_dist: number, + index: number, + elems: rustdoc.QueryElement[], + returned: rustdoc.QueryElement[], + is_alias: boolean, + alias?: string, + original?: rustdoc.Rlow, } /** @@ -364,7 +430,7 @@ declare namespace rustdoc { * Numeric IDs are *ONE-indexed* into the paths array (`p`). Zero is used as a sentinel for `null` * because `null` is four bytes while `0` is one byte. */ - type RawFunctionType = number | [number, Array<RawFunctionType>]; + type RawFunctionType = number | [number, Array<RawFunctionType>] | [number, Array<RawFunctionType>, Array<[RawFunctionType, RawFunctionType[]]>]; /** * The type signature entry in the decoded search index. @@ -382,8 +448,8 @@ declare namespace rustdoc { */ interface FunctionType { id: null|number, - ty: number|null, - name?: string, + ty: ItemType, + name: string|null, path: string|null, exactPath: string|null, unboxFlag: boolean, @@ -403,70 +469,6 @@ declare namespace rustdoc { bindings: Map<number, FingerprintableType[]>; }; - /** - * The raw search data for a given crate. `n`, `t`, `d`, `i`, and `f` - * are arrays with the same length. `q`, `a`, and `c` use a sparse - * representation for compactness. - * - * `n[i]` contains the name of an item. - * - * `t[i]` contains the type of that item - * (as a string of characters that represent an offset in `itemTypes`). - * - * `d[i]` contains the description of that item. - * - * `q` contains the full paths of the items. For compactness, it is a set of - * (index, path) pairs used to create a map. If a given index `i` is - * not present, this indicates "same as the last index present". - * - * `i[i]` contains an item's parent, usually a module. For compactness, - * it is a set of indexes into the `p` array. - * - * `f` contains function signatures, or `0` if the item isn't a function. - * More information on how they're encoded can be found in rustc-dev-guide - * - * Functions are themselves encoded as arrays. The first item is a list of - * types representing the function's inputs, and the second list item is a list - * of types representing the function's output. Tuples are flattened. - * Types are also represented as arrays; the first item is an index into the `p` - * array, while the second is a list of types representing any generic parameters. - * - * b[i] contains an item's impl disambiguator. This is only present if an item - * is defined in an impl block and, the impl block's type has more than one associated - * item with the same name. - * - * `a` defines aliases with an Array of pairs: [name, offset], where `offset` - * points into the n/t/d/q/i/f arrays. - * - * `doc` contains the description of the crate. - * - * `p` is a list of path/type pairs. It is used for parents and function parameters. - * The first item is the type, the second is the name, the third is the visible path (if any) and - * the fourth is the canonical path used for deduplication (if any). - * - * `r` is the canonical path used for deduplication of re-exported items. - * It is not used for associated items like methods (that's the fourth element - * of `p`) but is used for modules items like free functions. - * - * `c` is an array of item indices that are deprecated. - */ - type RawSearchIndexCrate = { - doc: string, - a: { [key: string]: number[] }, - n: Array<string>, - t: string, - D: string, - e: string, - q: Array<[number, string]>, - i: string, - f: string, - p: Array<[number, string] | [number, string, number] | [number, string, number, number] | [number, string, number, number, string]>, - b: Array<[number, String]>, - c: string, - r: Array<[number, number]>, - P: Array<[number, string]>, - }; - type VlqData = VlqData[] | number; /** diff --git a/src/librustdoc/html/static/js/search.js b/src/librustdoc/html/static/js/search.js index 2caf214ff73d4..f9a9732212b24 100644 --- a/src/librustdoc/html/static/js/search.js +++ b/src/librustdoc/html/static/js/search.js @@ -1,9 +1,16 @@ // ignore-tidy-filelength -/* global addClass, getNakedUrl, getSettingValue, getVar */ -/* global onEachLazy, removeClass, searchState, browserSupportsHistoryApi, exports */ +/* global addClass, getNakedUrl, getVar, nonnull, getSettingValue */ +/* global onEachLazy, removeClass, searchState, browserSupportsHistoryApi */ "use strict"; +/** + * @param {stringdex.Stringdex} Stringdex + * @param {typeof stringdex.RoaringBitmap} RoaringBitmap + * @param {stringdex.Hooks} hooks + */ +const initSearch = async function(Stringdex, RoaringBitmap, hooks) { + // polyfill // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/toSpliced if (!Array.prototype.toSpliced) { @@ -20,31 +27,65 @@ if (!Array.prototype.toSpliced) { * * @template T * @param {Iterable<T>} arr - * @param {function(T): any} func + * @param {function(T): Promise<any>} func * @param {function(T): boolean} funcBtwn */ -function onEachBtwn(arr, func, funcBtwn) { +async function onEachBtwnAsync(arr, func, funcBtwn) { let skipped = true; for (const value of arr) { if (!skipped) { funcBtwn(value); } - skipped = func(value); + skipped = await func(value); } } /** - * Convert any `undefined` to `null`. - * - * @template T - * @param {T|undefined} x - * @returns {T|null} + * Allow the browser to redraw. + * @returns {Promise<void>} */ -function undef2null(x) { - if (x !== undefined) { - return x; - } - return null; +const yieldToBrowser = typeof window !== "undefined" && window.requestIdleCallback ? + function() { + return new Promise((resolve, _reject) => { + window.requestIdleCallback(resolve); + }); + } : + function() { + return new Promise((resolve, _reject) => { + setTimeout(resolve, 0); + }); + }; + +/** + * Promise-based timer wrapper. + * @param {number} ms + * @returns {Promise<void>} + */ +const timeout = function(ms) { + return new Promise((resolve, _reject) => { + setTimeout(resolve, ms); + }); +}; + +if (!Promise.withResolvers) { + /** + * Polyfill + * @template T + * @returns {{ + "promise": Promise<T>, + "resolve": (function(T): void), + "reject": (function(any): void) + }} + */ + Promise.withResolvers = () => { + let resolve, reject; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + // @ts-expect-error + return {promise, resolve, reject}; + }; } // ==================== Core search logic begin ==================== @@ -81,13 +122,22 @@ 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 TY_FN = itemTypes.indexOf("fn"); -const TY_METHOD = itemTypes.indexOf("method"); -const TY_TYMETHOD = itemTypes.indexOf("tymethod"); +/** @type {rustdoc.ItemType} */ +const TY_PRIMITIVE = 1; +/** @type {rustdoc.ItemType} */ +const TY_GENERIC = 26; +/** @type {rustdoc.ItemType} */ +const TY_IMPORT = 4; +/** @type {rustdoc.ItemType} */ +const TY_TRAIT = 10; +/** @type {rustdoc.ItemType} */ +const TY_FN = 7; +/** @type {rustdoc.ItemType} */ +const TY_METHOD = 13; +/** @type {rustdoc.ItemType} */ +const TY_TYMETHOD = 12; +/** @type {rustdoc.ItemType} */ +const TY_ASSOCTYPE = 17; const ROOT_PATH = typeof window !== "undefined" ? window.rootPath : "../"; // Hard limit on how deep to recurse into generics when doing type-driven search. @@ -242,7 +292,7 @@ function isEndCharacter(c) { } /** - * @param {number} ty + * @param {rustdoc.ItemType} ty * @returns */ function isFnLikeTy(ty) { @@ -830,7 +880,7 @@ function createQueryElement(query, parserState, name, generics, isInGenerics) { */ function makePrimitiveElement(name, extra) { return Object.assign({ - name, + name: name, id: null, fullPath: [name], pathWithoutLast: [], @@ -1023,6 +1073,7 @@ class VlqHexDecoder { this.string = string; this.cons = cons; this.offset = 0; + this.elemCount = 0; /** @type {T[]} */ this.backrefQueue = []; } @@ -1060,6 +1111,7 @@ class VlqHexDecoder { n = (n << 4) | (c & 0xF); const [sign, value] = [n & 1, n >> 1]; this.offset += 1; + this.elemCount += 1; return sign ? -value : value; } /** @@ -1086,1303 +1138,149 @@ class VlqHexDecoder { return result; } } -class RoaringBitmap { - /** @param {string} str */ - constructor(str) { - // https://github.com/RoaringBitmap/RoaringFormatSpec - // - // Roaring bitmaps are used for flags that can be kept in their - // compressed form, even when loaded into memory. This decoder - // turns the containers into objects, but uses byte array - // slices of the original format for the data payload. - const strdecoded = atob(str); - const u8array = new Uint8Array(strdecoded.length); - for (let j = 0; j < strdecoded.length; ++j) { - u8array[j] = strdecoded.charCodeAt(j); - } - const has_runs = u8array[0] === 0x3b; - const size = has_runs ? - ((u8array[2] | (u8array[3] << 8)) + 1) : - ((u8array[4] | (u8array[5] << 8) | (u8array[6] << 16) | (u8array[7] << 24))); - let i = has_runs ? 4 : 8; - let is_run; - if (has_runs) { - const is_run_len = Math.floor((size + 7) / 8); - is_run = u8array.slice(i, i + is_run_len); - i += is_run_len; - } else { - is_run = new Uint8Array(); - } - this.keys = []; - this.cardinalities = []; - for (let j = 0; j < size; ++j) { - this.keys.push(u8array[i] | (u8array[i + 1] << 8)); - i += 2; - this.cardinalities.push((u8array[i] | (u8array[i + 1] << 8)) + 1); - i += 2; - } - this.containers = []; - let offsets = null; - if (!has_runs || this.keys.length >= 4) { - offsets = []; - for (let j = 0; j < size; ++j) { - offsets.push(u8array[i] | (u8array[i + 1] << 8) | (u8array[i + 2] << 16) | - (u8array[i + 3] << 24)); - i += 4; - } - } - for (let j = 0; j < size; ++j) { - if (offsets && offsets[j] !== i) { - // eslint-disable-next-line no-console - console.log(this.containers); - throw new Error(`corrupt bitmap ${j}: ${i} / ${offsets[j]}`); - } - if (is_run[j >> 3] & (1 << (j & 0x7))) { - const runcount = (u8array[i] | (u8array[i + 1] << 8)); - i += 2; - this.containers.push(new RoaringBitmapRun( - runcount, - u8array.slice(i, i + (runcount * 4)), - )); - i += runcount * 4; - } else if (this.cardinalities[j] >= 4096) { - this.containers.push(new RoaringBitmapBits(u8array.slice(i, i + 8192))); - i += 8192; - } else { - const end = this.cardinalities[j] * 2; - this.containers.push(new RoaringBitmapArray( - this.cardinalities[j], - u8array.slice(i, i + end), - )); - i += end; - } - } - } - /** @param {number} keyvalue */ - contains(keyvalue) { - const key = keyvalue >> 16; - const value = keyvalue & 0xFFFF; - // Binary search algorithm copied from - // https://en.wikipedia.org/wiki/Binary_search#Procedure - // - // Format is required by specification to be sorted. - // Because keys are 16 bits and unique, length can't be - // bigger than 2**16, and because we have 32 bits of safe int, - // left + right can't overflow. - let left = 0; - let right = this.keys.length - 1; - while (left <= right) { - const mid = Math.floor((left + right) / 2); - const x = this.keys[mid]; - if (x < key) { - left = mid + 1; - } else if (x > key) { - right = mid - 1; - } else { - return this.containers[mid].contains(value); - } - } - return false; - } -} -class RoaringBitmapRun { - /** - * @param {number} runcount - * @param {Uint8Array} array - */ - constructor(runcount, array) { - this.runcount = runcount; - this.array = array; - } - /** @param {number} value */ - contains(value) { - // Binary search algorithm copied from - // https://en.wikipedia.org/wiki/Binary_search#Procedure - // - // Since runcount is stored as 16 bits, left + right - // can't overflow. - let left = 0; - let right = this.runcount - 1; - while (left <= right) { - const mid = Math.floor((left + right) / 2); - const i = mid * 4; - const start = this.array[i] | (this.array[i + 1] << 8); - const lenm1 = this.array[i + 2] | (this.array[i + 3] << 8); - if ((start + lenm1) < value) { - left = mid + 1; - } else if (start > value) { - right = mid - 1; - } else { - return true; - } - } - return false; - } -} -class RoaringBitmapArray { - /** - * @param {number} cardinality - * @param {Uint8Array} array - */ - constructor(cardinality, array) { - this.cardinality = cardinality; - this.array = array; - } - /** @param {number} value */ - contains(value) { - // Binary search algorithm copied from - // https://en.wikipedia.org/wiki/Binary_search#Procedure - // - // Since cardinality can't be higher than 4096, left + right - // cannot overflow. - let left = 0; - let right = this.cardinality - 1; - while (left <= right) { - const mid = Math.floor((left + right) / 2); - const i = mid * 2; - const x = this.array[i] | (this.array[i + 1] << 8); - if (x < value) { - left = mid + 1; - } else if (x > value) { - right = mid - 1; - } else { - return true; - } - } - return false; - } -} -class RoaringBitmapBits { - /** - * @param {Uint8Array} array - */ - constructor(array) { - this.array = array; - } - /** @param {number} value */ - contains(value) { - return !!(this.array[value >> 3] & (1 << (value & 7))); - } -} +/** @type {Array<string>} */ +const EMPTY_STRING_ARRAY = []; + +/** @type {Array<rustdoc.FunctionType>} */ +const EMPTY_GENERICS_ARRAY = []; + +/** @type {Array<[number, rustdoc.FunctionType[]]>} */ +const EMPTY_BINDINGS_ARRAY = []; + +/** @type {Map<number, Array<any>>} */ +const EMPTY_BINDINGS_MAP = new Map(); /** - * A prefix tree, used for name-based search. - * - * This data structure is used to drive prefix matches, - * such as matching the query "link" to `LinkedList`, - * and Lev-distance matches, such as matching the - * query "hahsmap" to `HashMap`. Substring matches, - * such as "list" to `LinkedList`, are done with a - * tailTable that deep-links into this trie. - * - * children - * : A [sparse array] of subtrees. The array index - * is a charCode. - * - * [sparse array]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/ - * Indexed_collections#sparse_arrays - * - * matches - * : A list of search index IDs for this node. - * - * @type {{ - * children: NameTrie[], - * matches: number[], - * }} + * @param {string|null} typename + * @returns {number} */ -class NameTrie { - constructor() { - this.children = []; - this.matches = []; - } - /** - * @param {string} name - * @param {number} id - * @param {Map<string, NameTrie[]>} tailTable - */ - insert(name, id, tailTable) { - this.insertSubstring(name, 0, id, tailTable); - } - /** - * @param {string} name - * @param {number} substart - * @param {number} id - * @param {Map<string, NameTrie[]>} tailTable - */ - insertSubstring(name, substart, id, tailTable) { - const l = name.length; - if (substart === l) { - this.matches.push(id); - } else { - const sb = name.charCodeAt(substart); - let child; - if (this.children[sb] !== undefined) { - child = this.children[sb]; - } else { - child = new NameTrie(); - this.children[sb] = child; - /** @type {NameTrie[]} */ - let sste; - if (substart >= 2) { - const tail = name.substring(substart - 2, substart + 1); - const entry = tailTable.get(tail); - if (entry !== undefined) { - sste = entry; - } else { - sste = []; - tailTable.set(tail, sste); - } - sste.push(child); - } - } - child.insertSubstring(name, substart + 1, id, tailTable); - } - } - /** - * @param {string} name - * @param {Map<string, NameTrie[]>} tailTable - */ - search(name, tailTable) { - const results = new Set(); - this.searchSubstringPrefix(name, 0, results); - if (results.size < MAX_RESULTS && name.length >= 3) { - const levParams = name.length >= 6 ? - new Lev2TParametricDescription(name.length) : - new Lev1TParametricDescription(name.length); - this.searchLev(name, 0, levParams, results); - const tail = name.substring(0, 3); - const list = tailTable.get(tail); - if (list !== undefined) { - for (const entry of list) { - entry.searchSubstringPrefix(name, 3, results); - } - } - } - return [...results]; - } - /** - * @param {string} name - * @param {number} substart - * @param {Set<number>} results - */ - searchSubstringPrefix(name, substart, results) { - const l = name.length; - if (substart === l) { - for (const match of this.matches) { - results.add(match); - } - // breadth-first traversal orders prefix matches by length - /** @type {NameTrie[]} */ - let unprocessedChildren = []; - for (const child of this.children) { - if (child) { - unprocessedChildren.push(child); - } - } - /** @type {NameTrie[]} */ - let nextSet = []; - while (unprocessedChildren.length !== 0) { - /** @type {NameTrie} */ - // @ts-expect-error - const next = unprocessedChildren.pop(); - for (const child of next.children) { - if (child) { - nextSet.push(child); - } - } - for (const match of next.matches) { - results.add(match); - } - if (unprocessedChildren.length === 0) { - const tmp = unprocessedChildren; - unprocessedChildren = nextSet; - nextSet = tmp; - } - } - } else { - const sb = name.charCodeAt(substart); - if (this.children[sb] !== undefined) { - this.children[sb].searchSubstringPrefix(name, substart + 1, results); - } - } +function itemTypeFromName(typename) { + if (typename === null) { + return NO_TYPE_FILTER; } - /** - * @param {string} name - * @param {number} substart - * @param {Lev2TParametricDescription|Lev1TParametricDescription} levParams - * @param {Set<number>} results - */ - searchLev(name, substart, levParams, results) { - const stack = [[this, 0]]; - const n = levParams.n; - while (stack.length !== 0) { - // It's not empty - //@ts-expect-error - const [trie, levState] = stack.pop(); - for (const [charCode, child] of trie.children.entries()) { - if (!child) { - continue; - } - const levPos = levParams.getPosition(levState); - const vector = levParams.getVector( - name, - charCode, - levPos, - Math.min(name.length, levPos + (2 * n) + 1), - ); - const newLevState = levParams.transition( - levState, - levPos, - vector, - ); - if (newLevState >= 0) { - stack.push([child, newLevState]); - if (levParams.isAccept(newLevState)) { - for (const match of child.matches) { - results.add(match); - } - } - } - } - } + const index = itemTypes.findIndex(i => i === typename); + if (index < 0) { + throw ["Unknown type filter ", typename]; } + return index; } class DocSearch { /** - * @param {Map<string, rustdoc.RawSearchIndexCrate>} rawSearchIndex * @param {string} rootPath - * @param {rustdoc.SearchState} searchState + * @param {stringdex.Database} database */ - constructor(rawSearchIndex, rootPath, searchState) { - /** - * @type {Map<String, RoaringBitmap>} - */ - this.searchIndexDeprecated = new Map(); - /** - * @type {Map<String, RoaringBitmap>} - */ - this.searchIndexEmptyDesc = new Map(); - /** - * @type {Uint32Array} - */ - this.functionTypeFingerprint = new Uint32Array(0); - /** - * Map from normalized type names to integers. Used to make type search - * more efficient. - * - * @type {Map<string, {id: number, assocOnly: boolean}>} - */ - this.typeNameIdMap = new Map(); - /** - * Map from type ID to associated type name. Used for display, - * not for search. - * - * @type {Map<number, string>} - */ - this.assocTypeIdNameMap = new Map(); - this.ALIASES = new Map(); - this.FOUND_ALIASES = new Set(); + constructor(rootPath, database) { this.rootPath = rootPath; - this.searchState = searchState; + this.database = database; - /** - * Special type name IDs for searching by array. - * @type {number} - */ - this.typeNameIdOfArray = this.buildTypeMapIndex("array"); - /** - * Special type name IDs for searching by slice. - * @type {number} - */ - this.typeNameIdOfSlice = this.buildTypeMapIndex("slice"); - /** - * Special type name IDs for searching by both array and slice (`[]` syntax). - * @type {number} - */ - this.typeNameIdOfArrayOrSlice = this.buildTypeMapIndex("[]"); - /** - * Special type name IDs for searching by tuple. - * @type {number} - */ - this.typeNameIdOfTuple = this.buildTypeMapIndex("tuple"); - /** - * Special type name IDs for searching by unit. - * @type {number} - */ - this.typeNameIdOfUnit = this.buildTypeMapIndex("unit"); - /** - * Special type name IDs for searching by both tuple and unit (`()` syntax). - * @type {number} - */ - this.typeNameIdOfTupleOrUnit = this.buildTypeMapIndex("()"); - /** - * Special type name IDs for searching `fn`. - * @type {number} - */ - this.typeNameIdOfFn = this.buildTypeMapIndex("fn"); - /** - * Special type name IDs for searching `fnmut`. - * @type {number} - */ - this.typeNameIdOfFnMut = this.buildTypeMapIndex("fnmut"); - /** - * Special type name IDs for searching `fnonce`. - * @type {number} - */ - this.typeNameIdOfFnOnce = this.buildTypeMapIndex("fnonce"); - /** - * Special type name IDs for searching higher order functions (`->` syntax). - * @type {number} - */ - this.typeNameIdOfHof = this.buildTypeMapIndex("->"); - /** - * Special type name IDs the output assoc type. - * @type {number} - */ - this.typeNameIdOfOutput = this.buildTypeMapIndex("output", true); - /** - * Special type name IDs for searching by reference. - * @type {number} - */ - this.typeNameIdOfReference = this.buildTypeMapIndex("reference"); + this.typeNameIdOfOutput = -1; + this.typeNameIdOfArray = -1; + this.typeNameIdOfSlice = -1; + this.typeNameIdOfArrayOrSlice = -1; + this.typeNameIdOfTuple = -1; + this.typeNameIdOfUnit = -1; + this.typeNameIdOfTupleOrUnit = -1; + this.typeNameIdOfReference = -1; + this.typeNameIdOfHof = -1; - /** - * Empty, immutable map used in item search types with no bindings. - * - * @type {Map<number, Array<any>>} - */ - this.EMPTY_BINDINGS_MAP = new Map(); - - /** - * Empty, immutable map used in item search types with no bindings. - * - * @type {Array<any>} - */ - this.EMPTY_GENERICS_ARRAY = []; + this.utf8decoder = new TextDecoder(); - /** - * Object pool for function types with no bindings or generics. - * This is reset after loading the index. - * - * @type {Map<number|null, rustdoc.FunctionType>} - */ + /** @type {Map<number|null, rustdoc.FunctionType>} */ this.TYPES_POOL = new Map(); - - /** - * A trie for finding items by name. - * This is used for edit distance and prefix finding. - * - * @type {NameTrie} - */ - this.nameTrie = new NameTrie(); - - /** - * Find items by 3-substring. This is a map from three-char - * prefixes into lists of subtries. - */ - this.tailTable = new Map(); - - /** - * @type {Array<rustdoc.Row>} - */ - this.searchIndex = this.buildIndex(rawSearchIndex); } /** - * Add an item to the type Name->ID map, or, if one already exists, use it. - * Returns the number. If name is "" or null, return null (pure generic). - * - * This is effectively string interning, so that function matching can be - * done more quickly. Two types with the same name but different item kinds - * get the same ID. - * - * @template T extends string - * @overload - * @param {T} name - * @param {boolean=} isAssocType - True if this is an assoc type - * @returns {T extends "" ? null : number} - * - * @param {string} name - * @param {boolean=} isAssocType - * @returns {number | null} - * + * Load search index. If you do not call this function, `execQuery` + * will never fulfill. */ - buildTypeMapIndex(name, isAssocType) { - if (name === "" || name === null) { - return null; - } - - const obj = this.typeNameIdMap.get(name); - if (obj !== undefined) { - obj.assocOnly = !!(isAssocType && obj.assocOnly); - return obj.id; - } else { - const id = this.typeNameIdMap.size; - this.typeNameIdMap.set(name, { id, assocOnly: !!isAssocType }); - return id; + async buildIndex() { + const nn = this.database.getIndex("normalizedName"); + if (!nn) { + return; } + const [ + output, + fn, + fnMut, + fnOnce, + array, + slice, + arrayOrSlice, + tuple, + unit, + tupleOrUnit, + reference, + hof, + never, + ] = await Promise.all([ + nn.search("output"), + nn.search("fn"), + nn.search("fnmut"), + nn.search("fnonce"), + nn.search("array"), + nn.search("slice"), + nn.search("[]"), + nn.search("tuple"), + nn.search("unit"), + nn.search("()"), + nn.search("reference"), + nn.search("->"), + nn.search("never"), + ]); + /** + * @param {stringdex.Trie|null|undefined} trie + * @param {rustdoc.ItemType} ty + * @param {string} modulePath + * @returns {Promise<number>} + * */ + const first = async(trie, ty, modulePath) => { + if (trie) { + for (const id of trie.matches().entries()) { + const pathData = await this.getPathData(id); + if (pathData && pathData.ty === ty && pathData.modulePath === modulePath) { + return id; + } + } + } + return -1; + }; + this.typeNameIdOfOutput = await first(output, TY_ASSOCTYPE, ""); + this.typeNameIdOfFnPtr = await first(fn, TY_PRIMITIVE, ""); + this.typeNameIdOfFn = await first(fn, TY_TRAIT, "core::ops"); + this.typeNameIdOfFnMut = await first(fnMut, TY_TRAIT, "core::ops"); + this.typeNameIdOfFnOnce = await first(fnOnce, TY_TRAIT, "core::ops"); + this.typeNameIdOfArray = await first(array, TY_PRIMITIVE, ""); + this.typeNameIdOfSlice = await first(slice, TY_PRIMITIVE, ""); + this.typeNameIdOfArrayOrSlice = await first(arrayOrSlice, TY_PRIMITIVE, ""); + this.typeNameIdOfTuple = await first(tuple, TY_PRIMITIVE, ""); + this.typeNameIdOfUnit = await first(unit, TY_PRIMITIVE, ""); + this.typeNameIdOfTupleOrUnit = await first(tupleOrUnit, TY_PRIMITIVE, ""); + this.typeNameIdOfReference = await first(reference, TY_PRIMITIVE, ""); + this.typeNameIdOfHof = await first(hof, TY_PRIMITIVE, ""); + this.typeNameIdOfNever = await first(never, TY_PRIMITIVE, ""); } /** - * Convert a list of RawFunctionType / ID to object-based FunctionType. - * - * Crates often have lots of functions in them, and it's common to have a large number of - * functions that operate on a small set of data types, so the search index compresses them - * by encoding function parameter and return types as indexes into an array of names. - * - * Even when a general-purpose compression algorithm is used, this is still a win. - * I checked. https://github.com/rust-lang/rust/pull/98475#issue-1284395985 + * Parses the query. * - * The format for individual function types is encoded in - * librustdoc/html/render/mod.rs: impl Serialize for RenderType + * The supported syntax by this parser is given in the rustdoc book chapter + * /src/doc/rustdoc/src/read-documentation/search.md * - * @param {null|Array<rustdoc.RawFunctionType>} types - * @param {Array<{ - * name: string, - * ty: number, - * path: string|null, - * exactPath: string|null, - * unboxFlag: boolean - * }>} paths - * @param {Array<{ - * name: string, - * ty: number, - * path: string|null, - * exactPath: string|null, - * unboxFlag: boolean, - * }>} lowercasePaths + * When adding new things to the parser, add them there, too! * - * @return {Array<rustdoc.FunctionType>} - */ - buildItemSearchTypeAll(types, paths, lowercasePaths) { - return types && types.length > 0 ? - types.map(type => this.buildItemSearchType(type, paths, lowercasePaths)) : - this.EMPTY_GENERICS_ARRAY; - } - - /** - * Converts a single type. + * @param {string} userQuery - The user query * - * @param {rustdoc.RawFunctionType} type - * @param {Array<{ - * name: string, - * ty: number, - * path: string|null, - * exactPath: string|null, - * unboxFlag: boolean - * }>} paths - * @param {Array<{ - * name: string, - * ty: number, - * path: string|null, - * exactPath: string|null, - * unboxFlag: boolean, - * }>} lowercasePaths - * @param {boolean=} isAssocType + * @return {rustdoc.ParsedQuery<rustdoc.ParserQueryElement>} - The parsed query */ - buildItemSearchType(type, paths, lowercasePaths, isAssocType) { - const PATH_INDEX_DATA = 0; - const GENERICS_DATA = 1; - const BINDINGS_DATA = 2; - let pathIndex, generics, bindings; - if (typeof type === "number") { - pathIndex = type; - generics = this.EMPTY_GENERICS_ARRAY; - bindings = this.EMPTY_BINDINGS_MAP; - } else { - pathIndex = type[PATH_INDEX_DATA]; - generics = this.buildItemSearchTypeAll( - type[GENERICS_DATA], - paths, - lowercasePaths, - ); - // @ts-expect-error - if (type.length > BINDINGS_DATA && type[BINDINGS_DATA].length > 0) { - // @ts-expect-error - bindings = new Map(type[BINDINGS_DATA].map(binding => { - const [assocType, constraints] = binding; - // Associated type constructors are represented sloppily in rustdoc's - // type search, to make the engine simpler. - // - // MyType<Output<T>=Result<T>> is equivalent to MyType<Output<Result<T>>=T> - // and both are, essentially - // MyType<Output=(T, Result<T>)>, except the tuple isn't actually there. - // It's more like the value of a type binding is naturally an array, - // which rustdoc calls "constraints". - // - // As a result, the key should never have generics on it. - return [ - this.buildItemSearchType(assocType, paths, lowercasePaths, true).id, - this.buildItemSearchTypeAll(constraints, paths, lowercasePaths), - ]; - })); - } else { - bindings = this.EMPTY_BINDINGS_MAP; - } - } + static parseQuery(userQuery) { /** - * @type {rustdoc.FunctionType} - */ - let result; - if (pathIndex < 0) { - // types less than 0 are generic parameters - // 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, - generics, - bindings, - unboxFlag: true, - }; - } else if (pathIndex === 0) { - // `0` is used as a sentinel because it's fewer bytes than `null` - result = { - id: null, - name: "", - ty: null, - path: null, - exactPath: null, - generics, - bindings, - unboxFlag: true, - }; - } else { - const item = lowercasePaths[pathIndex - 1]; - const id = this.buildTypeMapIndex(item.name, isAssocType); - if (isAssocType && id !== null) { - this.assocTypeIdNameMap.set(id, paths[pathIndex - 1].name); - } - result = { - id, - name: paths[pathIndex - 1].name, - ty: item.ty, - path: item.path, - exactPath: item.exactPath, - generics, - bindings, - unboxFlag: item.unboxFlag, - }; - } - const cr = this.TYPES_POOL.get(result.id); - if (cr) { - // Shallow equality check. Since this function is used - // to construct every type object, this should be mostly - // equivalent to a deep equality check, except if there's - // a conflict, we don't keep the old one around, so it's - // not a fully precise implementation of hashcons. - if (cr.generics.length === result.generics.length && - cr.generics !== result.generics && - cr.generics.every((x, i) => result.generics[i] === x) - ) { - result.generics = cr.generics; - } - if (cr.bindings.size === result.bindings.size && cr.bindings !== result.bindings) { - let ok = true; - for (const [k, v] of cr.bindings.entries()) { - // @ts-expect-error - const v2 = result.bindings.get(v); - if (!v2) { - ok = false; - break; - } - if (v !== v2 && v.length === v2.length && v.every((x, i) => v2[i] === x)) { - result.bindings.set(k, v); - } else if (v !== v2) { - ok = false; - break; - } - } - if (ok) { - result.bindings = cr.bindings; - } - } - if (cr.ty === result.ty && cr.path === result.path - && cr.bindings === result.bindings && cr.generics === result.generics - && cr.ty === result.ty && cr.name === result.name - && cr.unboxFlag === result.unboxFlag - ) { - return cr; - } - } - this.TYPES_POOL.set(result.id, result); - return result; - } - - /** - * Type fingerprints allow fast, approximate matching of types. - * - * This algo creates a compact representation of the type set using a Bloom filter. - * This fingerprint is used three ways: - * - * - It accelerates the matching algorithm by checking the function fingerprint against the - * query fingerprint. If any bits are set in the query but not in the function, it can't - * match. - * - * - The fourth section has the number of items in the set. - * This is the distance function, used for filtering and for sorting. - * - * [^1]: Distance is the relatively naive metric of counting the number of distinct items in - * the function that are not present in the query. - * - * @param {rustdoc.FingerprintableType} type - a single type - * @param {Uint32Array} output - write the fingerprint to this data structure: uses 128 bits - */ - buildFunctionTypeFingerprint(type, output) { - let input = type.id; - // All forms of `[]`/`()`/`->` get collapsed down to one thing in the bloom filter. - // Differentiating between arrays and slices, if the user asks for it, is - // still done in the matching algorithm. - if (input === this.typeNameIdOfArray || input === this.typeNameIdOfSlice) { - input = this.typeNameIdOfArrayOrSlice; - } - if (input === this.typeNameIdOfTuple || input === this.typeNameIdOfUnit) { - input = this.typeNameIdOfTupleOrUnit; - } - if (input === this.typeNameIdOfFn || input === this.typeNameIdOfFnMut || - input === this.typeNameIdOfFnOnce) { - input = this.typeNameIdOfHof; - } - /** - * http://burtleburtle.net/bob/hash/integer.html - * ~~ is toInt32. It's used before adding, so - * the number stays in safe integer range. - * @param {number} k - */ - const hashint1 = k => { - k = (~~k + 0x7ed55d16) + (k << 12); - k = (k ^ 0xc761c23c) ^ (k >>> 19); - k = (~~k + 0x165667b1) + (k << 5); - k = (~~k + 0xd3a2646c) ^ (k << 9); - k = (~~k + 0xfd7046c5) + (k << 3); - return (k ^ 0xb55a4f09) ^ (k >>> 16); - }; - /** @param {number} k */ - const hashint2 = k => { - k = ~k + (k << 15); - k ^= k >>> 12; - k += k << 2; - k ^= k >>> 4; - k = Math.imul(k, 2057); - return k ^ (k >> 16); - }; - if (input !== null) { - const h0a = hashint1(input); - const h0b = hashint2(input); - // Less Hashing, Same Performance: Building a Better Bloom Filter - // doi=10.1.1.72.2442 - const h1a = ~~(h0a + Math.imul(h0b, 2)); - const h1b = ~~(h0a + Math.imul(h0b, 3)); - const h2a = ~~(h0a + Math.imul(h0b, 4)); - const h2b = ~~(h0a + Math.imul(h0b, 5)); - output[0] |= (1 << (h0a % 32)) | (1 << (h1b % 32)); - output[1] |= (1 << (h1a % 32)) | (1 << (h2b % 32)); - output[2] |= (1 << (h2a % 32)) | (1 << (h0b % 32)); - // output[3] is the total number of items in the type signature - output[3] += 1; - } - for (const g of type.generics) { - this.buildFunctionTypeFingerprint(g, output); - } - /** - * @type {{ - * id: number|null, - * ty: number, - * generics: rustdoc.FingerprintableType[], - * bindings: Map<number, rustdoc.FingerprintableType[]> - * }} - */ - const fb = { - id: null, - ty: 0, - generics: this.EMPTY_GENERICS_ARRAY, - bindings: this.EMPTY_BINDINGS_MAP, - }; - for (const [k, v] of type.bindings.entries()) { - fb.id = k; - fb.generics = v; - this.buildFunctionTypeFingerprint(fb, output); - } - } - - /** - * Convert raw search index into in-memory search index. - * - * @param {Map<string, rustdoc.RawSearchIndexCrate>} rawSearchIndex - * @returns {rustdoc.Row[]} - */ - buildIndex(rawSearchIndex) { - /** - * Convert from RawFunctionSearchType to FunctionSearchType. - * - * Crates often have lots of functions in them, and function signatures are sometimes - * complex, so rustdoc uses a pretty tight encoding for them. This function converts it - * to a simpler, object-based encoding so that the actual search code is more readable - * and easier to debug. - * - * 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, - * path: string|null, - * exactPath: string|null, - * unboxFlag: boolean - * }>} paths - * @param {Array<{ - * name: string, - * ty: number, - * path: string|null, - * exactPath: string|null, - * unboxFlag: boolean - * }>} lowercasePaths - * - * @return {function(rustdoc.RawFunctionSearchType): null|rustdoc.FunctionSearchType} - */ - const buildFunctionSearchTypeCallback = (paths, lowercasePaths) => { - /** - * @param {rustdoc.RawFunctionSearchType} functionSearchType - */ - const cb = functionSearchType => { - if (functionSearchType === 0) { - return null; - } - const INPUTS_DATA = 0; - const OUTPUT_DATA = 1; - /** @type {rustdoc.FunctionType[]} */ - let inputs; - /** @type {rustdoc.FunctionType[]} */ - let output; - if (typeof functionSearchType[INPUTS_DATA] === "number") { - inputs = [ - this.buildItemSearchType( - functionSearchType[INPUTS_DATA], - paths, - lowercasePaths, - ), - ]; - } else { - inputs = this.buildItemSearchTypeAll( - functionSearchType[INPUTS_DATA], - paths, - lowercasePaths, - ); - } - if (functionSearchType.length > 1) { - if (typeof functionSearchType[OUTPUT_DATA] === "number") { - output = [ - this.buildItemSearchType( - functionSearchType[OUTPUT_DATA], - paths, - lowercasePaths, - ), - ]; - } else { - output = this.buildItemSearchTypeAll( - // @ts-expect-error - functionSearchType[OUTPUT_DATA], - paths, - lowercasePaths, - ); - } - } else { - output = []; - } - const where_clause = []; - const l = functionSearchType.length; - for (let i = 2; i < l; ++i) { - where_clause.push(typeof functionSearchType[i] === "number" - // @ts-expect-error - ? [this.buildItemSearchType(functionSearchType[i], paths, lowercasePaths)] - : this.buildItemSearchTypeAll( - // @ts-expect-error - functionSearchType[i], - paths, - lowercasePaths, - )); - } - return { - inputs, output, where_clause, - }; - }; - return cb; - }; - - /** @type {rustdoc.Row[]} */ - const searchIndex = []; - let currentIndex = 0; - let id = 0; - - // Function type fingerprints are 128-bit bloom filters that are used to - // estimate the distance between function and query. - // This loop counts the number of items to allocate a fingerprint for. - for (const crate of rawSearchIndex.values()) { - // Each item gets an entry in the fingerprint array, and the crate - // does, too - id += crate.t.length + 1; - } - this.functionTypeFingerprint = new Uint32Array((id + 1) * 4); - // This loop actually generates the search item indexes, including - // normalized names, type signature objects and fingerprints, and aliases. - id = 0; - - /** @type {Array<[string, { [key: string]: Array<number> }, number]>} */ - const allAliases = []; - for (const [crate, crateCorpus] of rawSearchIndex) { - // a string representing the lengths of each description shard - // a string representing the list of function types - const itemDescShardDecoder = new VlqHexDecoder(crateCorpus.D, noop => { - /** @type {number} */ - // @ts-expect-error - const n = noop; - return n; - }); - let descShard = { - crate, - shard: 0, - start: 0, - len: itemDescShardDecoder.next(), - promise: null, - resolve: null, - }; - const descShardList = [descShard]; - - // Deprecated items and items with no description - this.searchIndexDeprecated.set(crate, new RoaringBitmap(crateCorpus.c)); - 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 - let normalizedName = crate.indexOf("_") === -1 ? crate : crate.replace(/_/g, ""); - const crateRow = { - crate, - ty: 3, // == ExternCrate - name: crate, - path: "", - descShard, - descIndex, - exactPath: "", - desc: crateCorpus.doc, - parent: undefined, - type: null, - paramNames: lastParamNames, - id, - word: crate, - normalizedName, - bitIndex: 0, - implDisambiguator: null, - }; - this.nameTrie.insert(normalizedName, id, this.tailTable); - id += 1; - searchIndex.push(crateRow); - currentIndex += 1; - // it's not undefined - // @ts-expect-error - if (!this.searchIndexEmptyDesc.get(crate).contains(0)) { - descIndex += 1; - } - - // see `RawSearchIndexCrate` in `rustdoc.d.ts` for a more - // up to date description of these fields - const itemTypes = crateCorpus.t; - // an array of (String) item names - const itemNames = crateCorpus.n; - // an array of [(Number) item index, - // (String) full path] - // an item whose index is not present will fall back to the previous present path - // i.e. if indices 4 and 11 are present, but 5-10 and 12-13 are not present, - // 5-10 will fall back to the path for 4 and 12-13 will fall back to the path for 11 - const itemPaths = new Map(crateCorpus.q); - // An array of [(Number) item index, (Number) path index] - // Used to de-duplicate inlined and re-exported stuff - const itemReexports = new Map(crateCorpus.r); - // an array of (Number) the parent path index + 1 to `paths`, or 0 if none - const itemParentIdxDecoder = new VlqHexDecoder(crateCorpus.i, noop => noop); - // a map Number, string for impl disambiguators - const implDisambiguator = new Map(crateCorpus.b); - const rawPaths = crateCorpus.p; - 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); - - /** - * @type {Array<{ - * name: string, - * ty: number, - * path: string|null, - * exactPath: string|null, - * unboxFlag: boolean - * }>} - */ - const lowercasePaths = []; - /** - * @type {Array<{ - * name: string, - * ty: number, - * path: string|null, - * exactPath: string|null, - * unboxFlag: boolean - * }>} - */ - const paths = []; - - // a string representing the list of function types - const itemFunctionDecoder = new VlqHexDecoder( - crateCorpus.f, - // @ts-expect-error - buildFunctionSearchTypeCallback(paths, lowercasePaths), - ); - - // convert `rawPaths` entries into object form - // generate normalizedPaths for function search mode - let len = rawPaths.length; - let lastPath = undef2null(itemPaths.get(0)); - for (let i = 0; i < len; ++i) { - const elem = rawPaths[i]; - const ty = elem[0]; - const name = elem[1]; - /** - * @param {2|3} idx - * @param {string|null} if_null - * @param {string|null} if_not_found - * @returns {string|null} - */ - const elemPath = (idx, if_null, if_not_found) => { - if (elem.length > idx && elem[idx] !== undefined) { - const p = itemPaths.get(elem[idx]); - if (p !== undefined) { - return p; - } - return if_not_found; - } - return if_null; - }; - const path = elemPath(2, lastPath, null); - const exactPath = elemPath(3, path, path); - const unboxFlag = elem.length > 4 && !!elem[4]; - - lowercasePaths.push({ ty, name: name.toLowerCase(), path, exactPath, unboxFlag }); - paths[i] = { ty, name, path, exactPath, unboxFlag }; - } - - // Convert `item*` into an object form, and construct word indices. - // - // Before any analysis is performed, let's gather the search terms to - // search against apart from the rest of the data. This is a quick - // operation that is cached for the life of the page state so that - // all other search operations have access to this cached data for - // faster analysis operations - lastPath = ""; - len = itemTypes.length; - let lastName = ""; - let lastWord = ""; - for (let i = 0; i < len; ++i) { - const bitIndex = i + 1; - if (descIndex >= descShard.len && - // @ts-expect-error - !this.searchIndexEmptyDesc.get(crate).contains(bitIndex)) { - descShard = { - crate, - shard: descShard.shard + 1, - start: descShard.start + descShard.len, - len: itemDescShardDecoder.next(), - promise: null, - resolve: null, - }; - descIndex = 0; - descShardList.push(descShard); - } - const name = itemNames[i] === "" ? lastName : itemNames[i]; - const word = itemNames[i] === "" ? lastWord : itemNames[i].toLowerCase(); - const pathU = itemPaths.get(i); - const path = pathU !== undefined ? pathU : lastPath; - const paramNameString = itemParamNames.get(i); - const paramNames = paramNameString !== undefined ? - paramNameString.split(",") : - lastParamNames; - const type = itemFunctionDecoder.next(); - if (type !== null) { - if (type) { - const fp = this.functionTypeFingerprint.subarray(id * 4, (id + 1) * 4); - for (const t of type.inputs) { - this.buildFunctionTypeFingerprint(t, fp); - } - for (const t of type.output) { - this.buildFunctionTypeFingerprint(t, fp); - } - for (const w of type.where_clause) { - for (const t of w) { - this.buildFunctionTypeFingerprint(t, fp); - } - } - } - } - // This object should have exactly the same set of fields as the "crateRow" - // object defined above. - const itemParentIdx = itemParentIdxDecoder.next(); - normalizedName = word.indexOf("_") === -1 ? word : word.replace(/_/g, ""); - /** @type {rustdoc.Row} */ - const row = { - crate, - ty: itemTypes.charCodeAt(i) - 65, // 65 = "A" - name, - path, - descShard, - descIndex, - exactPath: itemReexports.has(i) ? - // @ts-expect-error - itemPaths.get(itemReexports.get(i)) : path, - // @ts-expect-error - parent: itemParentIdx > 0 ? paths[itemParentIdx - 1] : undefined, - type, - paramNames, - id, - word, - normalizedName, - bitIndex, - implDisambiguator: undef2null(implDisambiguator.get(i)), - }; - this.nameTrie.insert(normalizedName, id, this.tailTable); - id += 1; - searchIndex.push(row); - lastPath = row.path; - lastParamNames = row.paramNames; - // @ts-expect-error - if (!this.searchIndexEmptyDesc.get(crate).contains(bitIndex)) { - descIndex += 1; - } - lastName = name; - lastWord = word; - } - - if (aliases) { - // We need to add the aliases in `searchIndex` after we finished filling it - // to not mess up indexes. - allAliases.push([crate, aliases, currentIndex]); - } - currentIndex += itemTypes.length; - this.searchState.descShards.set(crate, descShardList); - } - - for (const [crate, aliases, index] of allAliases) { - for (const [alias_name, alias_refs] of Object.entries(aliases)) { - if (!this.ALIASES.has(crate)) { - this.ALIASES.set(crate, new Map()); - } - const word = alias_name.toLowerCase(); - const crate_alias_map = this.ALIASES.get(crate); - if (!crate_alias_map.has(word)) { - crate_alias_map.set(word, []); - } - const aliases_map = crate_alias_map.get(word); - - const normalizedName = word.indexOf("_") === -1 ? word : word.replace(/_/g, ""); - for (const alias of alias_refs) { - const originalIndex = alias + index; - const original = searchIndex[originalIndex]; - /** @type {rustdoc.Row} */ - const row = { - crate, - name: alias_name, - normalizedName, - is_alias: true, - ty: original.ty, - type: original.type, - paramNames: [], - word, - id, - parent: undefined, - original, - path: "", - implDisambiguator: original.implDisambiguator, - // Needed to load the description of the original item. - // @ts-ignore - descShard: original.descShard, - descIndex: original.descIndex, - bitIndex: original.bitIndex, - }; - aliases_map.push(row); - this.nameTrie.insert(normalizedName, id, this.tailTable); - id += 1; - searchIndex.push(row); - } - } - } - // Drop the (rather large) hash table used for reusing function items - this.TYPES_POOL = new Map(); - return searchIndex; - } - - /** - * Parses the query. - * - * The supported syntax by this parser is given in the rustdoc book chapter - * /src/doc/rustdoc/src/read-documentation/search.md - * - * When adding new things to the parser, add them there, too! - * - * @param {string} userQuery - The user query - * - * @return {rustdoc.ParsedQuery<rustdoc.ParserQueryElement>} - The parsed query - */ - static parseQuery(userQuery) { - /** - * @param {string} typename - * @returns {number} - */ - function itemTypeFromName(typename) { - const index = itemTypes.findIndex(i => i === typename); - if (index < 0) { - throw ["Unknown type filter ", typename]; - } - return index; - } - - /** - * @param {rustdoc.ParserQueryElement} elem - */ - function convertTypeFilterOnElem(elem) { - if (typeof elem.typeFilter === "string") { - let typeFilter = elem.typeFilter; - if (typeFilter === "const") { - typeFilter = "constant"; - } - elem.typeFilter = itemTypeFromName(typeFilter); - } else { - elem.typeFilter = NO_TYPE_FILTER; - } - for (const elem2 of elem.generics) { - convertTypeFilterOnElem(elem2); - } - for (const constraints of elem.bindings.values()) { - for (const constraint of constraints) { - convertTypeFilterOnElem(constraint); - } - } - } - - /** - * Takes the user search input and returns an empty `ParsedQuery`. - * - * @param {string} userQuery - * - * @return {rustdoc.ParsedQuery<rustdoc.ParserQueryElement>} + * Takes the user search input and returns an empty `ParsedQuery`. + * + * @param {string} userQuery + * + * @return {rustdoc.ParsedQuery<rustdoc.ParserQueryElement>} */ function newParsedQuery(userQuery) { return { @@ -2437,8 +1335,8 @@ class DocSearch { continue; } if (!foundStopChar) { - /** @type String[] */ - let extra = []; + /** @type {string[]} */ + let extra = EMPTY_STRING_ARRAY; if (isLastElemGeneric(query.elems, parserState)) { extra = [" after ", ">"]; } else if (prevIs(parserState, "\"")) { @@ -2515,11 +1413,33 @@ class DocSearch { try { parseInput(query, parserState); + + // Scan for invalid type filters, so that we can report the error + // outside the search loop. + /** @param {rustdoc.ParserQueryElement} elem */ + const checkTypeFilter = elem => { + const ty = itemTypeFromName(elem.typeFilter); + if (ty === TY_GENERIC && elem.generics.length !== 0) { + throw [ + "Generic type parameter ", + elem.name, + " does not accept generic parameters", + ]; + } + for (const generic of elem.generics) { + checkTypeFilter(generic); + } + for (const constraints of elem.bindings.values()) { + for (const constraint of constraints) { + checkTypeFilter(constraint); + } + } + }; for (const elem of query.elems) { - convertTypeFilterOnElem(elem); + checkTypeFilter(elem); } for (const elem of query.returned) { - convertTypeFilterOnElem(elem); + checkTypeFilter(elem); } } catch (err) { query = newParsedQuery(userQuery); @@ -2543,208 +1463,574 @@ class DocSearch { } /** - * Executes the parsed query and builds a {ResultsTable}. - * - * @param {rustdoc.ParsedQuery<rustdoc.ParserQueryElement>} origParsedQuery - * - The parsed user query - * @param {Object} filterCrates - Crate to search in if defined - * @param {string} currentCrate - Current crate, to rank results from this crate higher - * - * @return {Promise<rustdoc.ResultsTable>} + * @param {number} id + * @returns {Promise<string|null>} */ - async execQuery(origParsedQuery, filterCrates, currentCrate) { - /** @type {rustdoc.Results} */ - const results_others = new Map(), - /** @type {rustdoc.Results} */ - results_in_args = new Map(), - /** @type {rustdoc.Results} */ - results_returned = new Map(); - - /** @type {rustdoc.ParsedQuery<rustdoc.QueryElement>} */ - // @ts-expect-error - const parsedQuery = origParsedQuery; + async getName(id) { + const ni = this.database.getData("name"); + if (!ni) { + return null; + } + const name = await ni.at(id); + return name === undefined || name === null ? null : this.utf8decoder.decode(name); + } - const queryLen = - parsedQuery.elems.reduce((acc, next) => acc + next.pathLast.length, 0) + - parsedQuery.returned.reduce((acc, next) => acc + next.pathLast.length, 0); - const maxEditDistance = Math.floor(queryLen / 3); - // We reinitialize the `FOUND_ALIASES` map. - this.FOUND_ALIASES.clear(); + /** + * @param {number} id + * @returns {Promise<string|null>} + */ + async getDesc(id) { + const di = this.database.getData("desc"); + if (!di) { + return null; + } + const desc = await di.at(id); + return desc === undefined || desc === null ? null : this.utf8decoder.decode(desc); + } + + /** + * @param {number} id + * @returns {Promise<number|null>} + */ + async getAliasPointer(id) { + const ai = this.database.getData("alias"); + if (!ai) { + return null; + } + const bytes = await ai.at(id); + if (bytes === undefined || bytes === null || bytes.length === 0) { + return null; + } else { + /** @type {string} */ + const encoded = this.utf8decoder.decode(bytes); + /** @type {number|null} */ + const decoded = JSON.parse(encoded); + return decoded; + } + } + + /** + * @param {number} id + * @returns {Promise<rustdoc.EntryData|null>} + */ + async getEntryData(id) { + const ei = this.database.getData("entry"); + if (!ei) { + return null; + } + const encoded = this.utf8decoder.decode(await ei.at(id)); + if (encoded === "" || encoded === undefined || encoded === null) { + return null; + } + /** + * krate, + * ty, + * module_path, + * exact_module_path, + * parent, + * deprecated, + * associated_item_disambiguator + * @type {[ + * number, + * rustdoc.ItemType, + * number, + * number, + * number, + * number, + * string, + * ] | [ + * number, + * rustdoc.ItemType, + * number, + * number, + * number, + * number, + * ]} + */ + const raw = JSON.parse(encoded); + return { + krate: raw[0], + ty: raw[1], + modulePath: raw[2] === 0 ? null : raw[2] - 1, + exactModulePath: raw[3] === 0 ? null : raw[3] - 1, + parent: raw[4] === 0 ? null : raw[4] - 1, + deprecated: raw[5] === 1 ? true : false, + associatedItemDisambiguator: raw.length === 6 ? null : raw[6], + }; + } + /** + * @param {number} id + * @returns {Promise<rustdoc.PathData|null>} + */ + async getPathData(id) { + const pi = this.database.getData("path"); + if (!pi) { + return null; + } + const encoded = this.utf8decoder.decode(await pi.at(id)); + if (encoded === "" || encoded === undefined || encoded === null) { + return null; + } /** - * @type {Map<string, number>} + * ty, module_path, exact_module_path, search_unbox, inverted_function_signature_index + * @type {[ + * rustdoc.ItemType, + * string, + * string|0, + * 0|1, + * string, + * ] | [ + * rustdoc.ItemType, + * string, + * string|0, + * 0|1, + * ] | [ + * rustdoc.ItemType, + * string, + * string, + * ] | [ + * rustdoc.ItemType, + * string, + * ]} */ - const genericSymbols = new Map(); + const raw = JSON.parse(encoded); + return { + ty: raw[0], + modulePath: raw[1], + exactModulePath: raw[2] === 0 || raw[2] === undefined ? raw[1] : raw[2], + }; + } + /** + * @param {number} id + * @returns {Promise<rustdoc.FunctionData|null>} + */ + async getFunctionData(id) { + const fi = this.database.getData("function"); + if (!fi) { + return null; + } + const encoded = this.utf8decoder.decode(await fi.at(id)); + if (encoded === "" || encoded === undefined || encoded === null) { + return null; + } /** - * Convert names to ids in parsed query elements. - * This is not used for the "In Names" tab, but is used for the - * "In Params", "In Returns", and "In Function Signature" tabs. - * - * If there is no matching item, but a close-enough match, this - * function also that correction. - * - * See `buildTypeMapIndex` for more information. - * - * @param {rustdoc.QueryElement} elem - * @param {boolean=} isAssocType + * function_signature, param_names + * @type {[string, string[]]} */ - const convertNameToId = (elem, isAssocType) => { - const loweredName = elem.pathLast.toLowerCase(); - if (this.typeNameIdMap.has(loweredName) && - // @ts-expect-error - (isAssocType || !this.typeNameIdMap.get(loweredName).assocOnly)) { - // @ts-expect-error - elem.id = this.typeNameIdMap.get(loweredName).id; - } else if (!parsedQuery.literalSearch) { - let match = null; - let matchDist = maxEditDistance + 1; - let matchName = ""; - for (const [name, { id, assocOnly }] of this.typeNameIdMap) { - const dist = Math.min( - editDistance(name, loweredName, maxEditDistance), - editDistance(name, elem.normalizedPathLast, maxEditDistance), - ); - if (dist <= matchDist && dist <= maxEditDistance && - (isAssocType || !assocOnly)) { - if (dist === matchDist && matchName > name) { - continue; - } - match = id; - matchDist = dist; - matchName = name; - } - } - if (match !== null) { - parsedQuery.correction = matchName; - } - elem.id = match; + const raw = JSON.parse(encoded); + + const parser = new VlqHexDecoder(raw[0], async functionSearchType => { + if (typeof functionSearchType === "number") { + return null; + } + const INPUTS_DATA = 0; + const OUTPUT_DATA = 1; + /** @type {Promise<rustdoc.FunctionType[]>} */ + let inputs_; + /** @type {Promise<rustdoc.FunctionType[]>} */ + let output_; + if (typeof functionSearchType[INPUTS_DATA] === "number") { + inputs_ = Promise.all([ + this.buildItemSearchType(functionSearchType[INPUTS_DATA]), + ]); + } else { + // @ts-ignore + inputs_ = this.buildItemSearchTypeAll(functionSearchType[INPUTS_DATA]); } - if ((elem.id === null && parsedQuery.totalElems > 1 && elem.typeFilter === -1 - && elem.generics.length === 0 && elem.bindings.size === 0) - || elem.typeFilter === TY_GENERIC) { - const id = genericSymbols.get(elem.normalizedPathLast); - if (id !== undefined) { - elem.id = id; + if (functionSearchType.length > 1) { + if (typeof functionSearchType[OUTPUT_DATA] === "number") { + output_ = Promise.all([ + this.buildItemSearchType(functionSearchType[OUTPUT_DATA]), + ]); } else { - elem.id = -(genericSymbols.size + 1); - genericSymbols.set(elem.normalizedPathLast, elem.id); - } - if (elem.typeFilter === -1 && elem.normalizedPathLast.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.normalizedPathLast.length / 3); - let matchDist = maxPartDistance + 1; - let matchName = ""; - for (const name of this.typeNameIdMap.keys()) { - const dist = editDistance( - name, - elem.normalizedPathLast, - maxPartDistance, - ); - if (dist <= matchDist && dist <= maxPartDistance) { - if (dist === matchDist && matchName > name) { - continue; - } - matchDist = dist; - matchName = name; - } - } - if (matchName !== "") { - parsedQuery.proposeCorrectionFrom = elem.name; - parsedQuery.proposeCorrectionTo = matchName; - } + // @ts-expect-error + output_ = this.buildItemSearchTypeAll(functionSearchType[OUTPUT_DATA]); } - elem.typeFilter = TY_GENERIC; - } - if (elem.generics.length > 0 && elem.typeFilter === TY_GENERIC) { - // Rust does not have HKT - parsedQuery.error = [ - "Generic type parameter ", - elem.name, - " does not accept generic parameters", - ]; - } - for (const elem2 of elem.generics) { - convertNameToId(elem2); + } else { + output_ = Promise.resolve(EMPTY_GENERICS_ARRAY); } - elem.bindings = new Map(Array.from(elem.bindings.entries()) - .map(entry => { - const [name, constraints] = entry; + /** @type {Promise<rustdoc.FunctionType[]>[]} */ + const where_clause_ = []; + const l = functionSearchType.length; + for (let i = 2; i < l; ++i) { + where_clause_.push(typeof functionSearchType[i] === "number" // @ts-expect-error - if (!this.typeNameIdMap.has(name)) { - parsedQuery.error = [ - "Type parameter ", - // @ts-expect-error - name, - " does not exist", - ]; - return [0, []]; - } - for (const elem2 of constraints) { - convertNameToId(elem2, false); - } - + ? Promise.all([this.buildItemSearchType(functionSearchType[i])]) // @ts-expect-error - return [this.typeNameIdMap.get(name).id, constraints]; - }), - ); + : this.buildItemSearchTypeAll(functionSearchType[i]), + ); + } + const [inputs, output, where_clause] = await Promise.all([ + inputs_, + output_, + Promise.all(where_clause_), + ]); + return { + inputs, output, where_clause, + }; + }); + + return { + functionSignature: await parser.next(), + paramNames: !raw[1] || raw[1].length === 0 ? EMPTY_STRING_ARRAY : raw[1], + elemCount: parser.elemCount, }; + } + + /** + * @param {number} id + * @returns {Promise<rustdoc.TypeData|null>} + */ + async getTypeData(id) { + const ti = this.database.getData("type"); + if (!ti) { + return null; + } + const encoded = this.utf8decoder.decode(await ti.at(id)); + if (encoded === "" || encoded === undefined || encoded === null) { + return null; + } + /** + * function_signature, param_names + * @type {[string, number] | [number] | [string] | [] | null} + */ + const raw = JSON.parse(encoded); + + if (!raw || raw[0] === undefined) { + return null; + } + + let searchUnbox = false; + let invertedFunctionSignatureIndex = RoaringBitmap.empty(); + + if (typeof raw[0] === "string") { + if (raw[1]) { + searchUnbox = true; + } + invertedFunctionSignatureIndex = raw[0] === undefined ? + RoaringBitmap.empty() : + new RoaringBitmap(makeUint8ArrayFromBase64(raw[0])); + } else if (raw[0]) { + searchUnbox = true; + } + + return { searchUnbox, invertedFunctionSignatureIndex }; + } + + /** + * @returns {Promise<string[]>} + */ + async getCrateNameList() { + const crateNames = this.database.getData("crateNames"); + if (!crateNames) { + return []; + } + const l = crateNames.length; + const names = []; + for (let i = 0; i < l; ++i) { + names.push(crateNames.at(i).then(name => { + if (name === undefined) { + return ""; + } + return this.utf8decoder.decode(name); + })); + } + return Promise.all(names); + } - for (const elem of parsedQuery.elems) { - convertNameToId(elem, false); - this.buildFunctionTypeFingerprint(elem, parsedQuery.typeFingerprint); + /** + * @param {number} id positive-numbered generic index (the AST uses negative-numbered ones) + * @returns {Promise<stringdex.RoaringBitmap>} + */ + async getGenericInvertedIndex(id) { + const gii = this.database.getData("generic_inverted_index"); + if (!gii) { + return RoaringBitmap.empty(); } - for (const elem of parsedQuery.returned) { - convertNameToId(elem, false); - this.buildFunctionTypeFingerprint(elem, parsedQuery.typeFingerprint); + const encoded = this.utf8decoder.decode( + await gii.at(id), + ); + if (encoded === "" || encoded === undefined || encoded === null) { + return RoaringBitmap.empty(); } + return new RoaringBitmap(makeUint8ArrayFromBase64(encoded)); + } + /** + * @param {number} id + * @returns {Promise<rustdoc.Row?>} + */ + async getRow(id) { + const [name_, entry, path, type] = await Promise.all([ + this.getName(id), + this.getEntryData(id), + this.getPathData(id), + this.getFunctionData(id), + ]); + if (!entry && !path) { + return null; + } + const [ + moduleName, + modulePathData, + exactModuleName, + exactModulePathData, + ] = await Promise.all([ + entry && entry.modulePath !== null ? this.getName(entry.modulePath) : null, + entry && entry.modulePath !== null ? this.getPathData(entry.modulePath) : null, + entry && entry.exactModulePath !== null ? + this.getName(entry.exactModulePath) : + null, + entry && entry.exactModulePath !== null ? + this.getPathData(entry.exactModulePath) : + null, + ]); + const name = name_ === null ? "" : name_; + const normalizedName = (name.indexOf("_") === -1 ? + name : + name.replace(/_/g, "")).toLowerCase(); + const modulePath = modulePathData === null || moduleName === null ? "" : + (modulePathData.modulePath === "" ? + moduleName : + `${modulePathData.modulePath}::${moduleName}`); + const [parentName, parentPath] = entry !== null && entry.parent !== null ? + await Promise.all([this.getName(entry.parent), this.getPathData(entry.parent)]) : + [null, null]; + return { + id, + crate: entry ? nonnull(await this.getName(entry.krate)) : "", + ty: entry ? entry.ty : nonnull(path).ty, + name, + normalizedName, + modulePath, + exactModulePath: exactModulePathData === null || exactModuleName === null ? modulePath : + (exactModulePathData.exactModulePath === "" ? + exactModuleName : + `${exactModulePathData.exactModulePath}::${exactModuleName}`), + entry, + path, + type, + deprecated: entry ? entry.deprecated : false, + parent: parentName !== null && parentPath !== null ? + { name: parentName, path: parentPath } : + null, + }; + } + + /** + * Convert a list of RawFunctionType / ID to object-based FunctionType. + * + * Crates often have lots of functions in them, and it's common to have a large number of + * functions that operate on a small set of data types, so the search index compresses them + * by encoding function parameter and return types as indexes into an array of names. + * + * Even when a general-purpose compression algorithm is used, this is still a win. + * I checked. https://github.com/rust-lang/rust/pull/98475#issue-1284395985 + * + * The format for individual function types is encoded in + * librustdoc/html/render/mod.rs: impl Serialize for RenderType + * + * @param {null|Array<rustdoc.RawFunctionType>} types + * + * @return {Promise<Array<rustdoc.FunctionType>>} + */ + async buildItemSearchTypeAll(types) { + return types && types.length > 0 ? + await Promise.all(types.map(type => this.buildItemSearchType(type))) : + EMPTY_GENERICS_ARRAY; + } + + /** + * Converts a single type. + * + * @param {rustdoc.RawFunctionType} type + * @return {Promise<rustdoc.FunctionType>} + */ + async buildItemSearchType(type) { + const PATH_INDEX_DATA = 0; + const GENERICS_DATA = 1; + const BINDINGS_DATA = 2; + let id, generics; /** - * Creates the query results. - * - * @param {Array<rustdoc.ResultObject>} results_in_args - * @param {Array<rustdoc.ResultObject>} results_returned - * @param {Array<rustdoc.ResultObject>} results_others - * @param {rustdoc.ParsedQuery<rustdoc.QueryElement>} parsedQuery - * - * @return {rustdoc.ResultsTable} + * @type {Map<number, rustdoc.FunctionType[]>} */ - function createQueryResults( - results_in_args, - results_returned, - results_others, - parsedQuery) { - return { - "in_args": results_in_args, - "returned": results_returned, - "others": results_others, - "query": parsedQuery, + let bindings; + if (typeof type === "number") { + id = type; + generics = EMPTY_GENERICS_ARRAY; + bindings = EMPTY_BINDINGS_MAP; + } else { + id = type[PATH_INDEX_DATA]; + generics = await this.buildItemSearchTypeAll(type[GENERICS_DATA]); + if (type[BINDINGS_DATA] && type[BINDINGS_DATA].length > 0) { + bindings = new Map((await Promise.all(type[BINDINGS_DATA].map( + /** + * @param {[rustdoc.RawFunctionType, rustdoc.RawFunctionType[]]} binding + * @returns {Promise<[number, rustdoc.FunctionType[]][]>} + */ + async binding => { + const [assocType, constraints] = binding; + // Associated type constructors are represented sloppily in rustdoc's + // type search, to make the engine simpler. + // + // MyType<Output<T>=Result<T>> is equivalent to MyType<Output<Result<T>>=T> + // and both are, essentially + // MyType<Output=(T, Result<T>)>, except the tuple isn't actually there. + // It's more like the value of a type binding is naturally an array, + // which rustdoc calls "constraints". + // + // As a result, the key should never have generics on it. + const [k, v] = await Promise.all([ + this.buildItemSearchType(assocType).then(t => t.id), + this.buildItemSearchTypeAll(constraints), + ]); + return k === null ? EMPTY_BINDINGS_ARRAY : [[k, v]]; + }, + ))).flat()); + } else { + bindings = EMPTY_BINDINGS_MAP; + } + } + /** + * @type {rustdoc.FunctionType} + */ + let result; + if (id < 0) { + // types less than 0 are generic parameters + // the actual names of generic parameters aren't stored, since they aren't API + result = { + id, + name: "", + ty: TY_GENERIC, + path: null, + exactPath: null, + generics, + bindings, + unboxFlag: true, }; + } else if (id === 0) { + // `0` is used as a sentinel because it's fewer bytes than `null` + result = { + id: null, + name: "", + ty: TY_GENERIC, + path: null, + exactPath: null, + generics, + bindings, + unboxFlag: true, + }; + } else { + const [name, path, type] = await Promise.all([ + this.getName(id - 1), + this.getPathData(id - 1), + this.getTypeData(id - 1), + ]); + if (path === undefined || path === null || type === undefined || type === null) { + return { + id: null, + name: "", + ty: TY_GENERIC, + path: null, + exactPath: null, + generics, + bindings, + unboxFlag: true, + }; + } + result = { + id: id - 1, + name, + ty: path.ty, + path: path.modulePath, + exactPath: path.exactModulePath === null ? path.modulePath : path.exactModulePath, + generics, + bindings, + unboxFlag: type.searchUnbox, + }; + } + const cr = this.TYPES_POOL.get(result.id); + if (cr) { + // Shallow equality check. Since this function is used + // to construct every type object, this should be mostly + // equivalent to a deep equality check, except if there's + // a conflict, we don't keep the old one around, so it's + // not a fully precise implementation of hashcons. + if (cr.generics.length === result.generics.length && + cr.generics !== result.generics && + cr.generics.every((x, i) => result.generics[i] === x) + ) { + result.generics = cr.generics; + } + if (cr.bindings.size === result.bindings.size && cr.bindings !== result.bindings) { + let ok = true; + for (const [k, v] of cr.bindings.entries()) { + const v2 = result.bindings.get(k); + if (!v2) { + ok = false; + break; + } + if (v !== v2 && v.length === v2.length && v.every((x, i) => v2[i] === x)) { + result.bindings.set(k, v); + } else if (v !== v2) { + ok = false; + break; + } + } + if (ok) { + result.bindings = cr.bindings; + } + } + if (cr.ty === result.ty && cr.path === result.path + && cr.bindings === result.bindings && cr.generics === result.generics + && cr.ty === result.ty && cr.name === result.name + && cr.unboxFlag === result.unboxFlag + ) { + return cr; + } } + this.TYPES_POOL.set(result.id, result); + return result; + } - // @ts-expect-error + /** + * Executes the parsed query and builds a {ResultsTable}. + * + * @param {rustdoc.ParsedQuery<rustdoc.ParserQueryElement>} parsedQuery + * - The parsed user query + * @param {Object} filterCrates - Crate to search in if defined + * @param {string} currentCrate - Current crate, to rank results from this crate higher + * + * @return {Promise<rustdoc.ResultsTable>} + */ + async execQuery(parsedQuery, filterCrates, currentCrate) { + const queryLen = + parsedQuery.elems.reduce((acc, next) => acc + next.pathLast.length, 0) + + parsedQuery.returned.reduce((acc, next) => acc + next.pathLast.length, 0); + const maxEditDistance = Math.floor(queryLen / 3); + + /** + * @param {rustdoc.Row} item + * @returns {[string, string, string]} + */ const buildHrefAndPath = item => { let displayPath; let href; - if (item.is_alias) { - this.FOUND_ALIASES.add(item.word); - item = item.original; - } const type = itemTypes[item.ty]; const name = item.name; - let path = item.path; - let exactPath = item.exactPath; + let path = item.modulePath; + let exactPath = item.exactModulePath; if (type === "mod") { displayPath = path + "::"; href = this.rootPath + path.replace(/::/g, "/") + "/" + name + "/index.html"; } else if (type === "import") { - displayPath = item.path + "::"; - href = this.rootPath + item.path.replace(/::/g, "/") + + displayPath = item.modulePath + "::"; + href = this.rootPath + item.modulePath.replace(/::/g, "/") + "/index.html#reexport." + name; } else if (type === "primitive" || type === "keyword") { displayPath = ""; @@ -2754,13 +2040,13 @@ class DocSearch { } else if (type === "externcrate") { displayPath = ""; href = this.rootPath + name + "/index.html"; - } else if (item.parent !== undefined) { + } else if (item.parent) { const myparent = item.parent; let anchor = type + "." + name; - const parentType = itemTypes[myparent.ty]; + const parentType = itemTypes[myparent.path.ty]; let pageType = parentType; let pageName = myparent.name; - exactPath = `${myparent.exactPath}::${myparent.name}`; + exactPath = `${myparent.path.exactModulePath}::${myparent.name}`; if (parentType === "primitive") { displayPath = myparent.name + "::"; @@ -2768,9 +2054,9 @@ class DocSearch { } else if (type === "structfield" && parentType === "variant") { // Structfields belonging to variants are special: the // final path element is the enum name. - const enumNameIdx = item.path.lastIndexOf("::"); - const enumName = item.path.substr(enumNameIdx + 2); - path = item.path.substr(0, enumNameIdx); + const enumNameIdx = item.modulePath.lastIndexOf("::"); + const enumName = item.modulePath.substr(enumNameIdx + 2); + path = item.modulePath.substr(0, enumNameIdx); displayPath = path + "::" + enumName + "::" + myparent.name + "::"; anchor = "variant." + myparent.name + ".field." + name; pageType = "enum"; @@ -2778,16 +2064,16 @@ class DocSearch { } else { displayPath = path + "::" + myparent.name + "::"; } - if (item.implDisambiguator !== null) { - anchor = item.implDisambiguator + "/" + anchor; + if (item.entry && item.entry.associatedItemDisambiguator !== null) { + anchor = item.entry.associatedItemDisambiguator + "/" + anchor; } href = this.rootPath + path.replace(/::/g, "/") + "/" + pageType + "." + pageName + ".html#" + anchor; } else { - displayPath = item.path + "::"; - href = this.rootPath + item.path.replace(/::/g, "/") + + displayPath = item.modulePath + "::"; + href = this.rootPath + item.modulePath.replace(/::/g, "/") + "/" + type + "." + name + ".html"; } return [displayPath, href, `${exactPath}::${name}`]; @@ -2810,38 +2096,49 @@ class DocSearch { * Add extra data to result objects, and filter items that have been * marked for removal. * - * @param {rustdoc.ResultObject[]} results + * @param {[rustdoc.PlainResultObject, rustdoc.Row][]} results * @param {"sig"|"elems"|"returned"|null} typeInfo + * @param {Set<string>} duplicates * @returns {rustdoc.ResultObject[]} */ - const transformResults = (results, typeInfo) => { - const duplicates = new Set(); + const transformResults = (results, typeInfo, duplicates) => { const out = []; - for (const result of results) { - if (result.id !== -1) { - const res = buildHrefAndPath(this.searchIndex[result.id]); + for (const [result, item] of results) { + if (item.id !== -1) { + const res = buildHrefAndPath(item); // many of these properties don't strictly need to be // copied over, but copying them over satisfies tsc, // and hopefully plays nice with the shape optimization // of the browser engine. /** @type {rustdoc.ResultObject} */ const obj = Object.assign({ - parent: result.parent, - type: result.type, + parent: item.parent ? { + path: item.parent.path.modulePath, + exactPath: item.parent.path.exactModulePath || + item.parent.path.modulePath, + name: item.parent.name, + ty: item.parent.path.ty, + } : undefined, + type: item.type && item.type.functionSignature ? + item.type.functionSignature : + undefined, + paramNames: item.type && item.type.paramNames ? + item.type.paramNames : + undefined, dist: result.dist, path_dist: result.path_dist, index: result.index, - desc: result.desc, - item: result.item, + desc: this.getDesc(result.id), + item, displayPath: pathSplitter(res[0]), fullPath: "", href: "", displayTypeSignature: null, - }, this.searchIndex[result.id]); + }, result); // To be sure than it some items aren't considered as duplicate. - obj.fullPath = res[2] + "|" + obj.ty; + obj.fullPath = res[2] + "|" + obj.item.ty; if (duplicates.has(obj.fullPath)) { continue; @@ -2849,7 +2146,7 @@ class DocSearch { // Exports are specifically not shown if the items they point at // are already in the results. - if (obj.ty === TY_IMPORT && duplicates.has(res[2])) { + if (obj.item.ty === TY_IMPORT && duplicates.has(res[2])) { continue; } if (duplicates.has(res[2] + "|" + TY_IMPORT)) { @@ -2859,9 +2156,13 @@ class DocSearch { duplicates.add(res[2]); if (typeInfo !== null) { - obj.displayTypeSignature = - // @ts-expect-error - this.formatDisplayTypeSignature(obj, typeInfo); + // @ts-expect-error + obj.displayTypeSignature = this.formatDisplayTypeSignature( + obj, + typeInfo, + result.elems, + result.returned, + ); } obj.href = res[1]; @@ -2883,9 +2184,11 @@ class DocSearch { * * @param {rustdoc.ResultObject} obj * @param {"sig"|"elems"|"returned"|null} typeInfo + * @param {rustdoc.QueryElement[]} elems + * @param {rustdoc.QueryElement[]} returned * @returns {Promise<rustdoc.DisplayTypeSignature>} */ - this.formatDisplayTypeSignature = async(obj, typeInfo) => { + this.formatDisplayTypeSignature = async(obj, typeInfo, elems, returned) => { const objType = obj.type; if (!objType) { return {type: [], mappedNames: new Map(), whereClause: new Map()}; @@ -2897,13 +2200,13 @@ class DocSearch { if (typeInfo !== "elems" && typeInfo !== "returned") { fnInputs = unifyFunctionTypes( objType.inputs, - parsedQuery.elems, + elems, objType.where_clause, null, mgensScratch => { fnOutput = unifyFunctionTypes( objType.output, - parsedQuery.returned, + returned, objType.where_clause, mgensScratch, mgensOut => { @@ -2917,10 +2220,9 @@ class DocSearch { 0, ); } else { - const arr = typeInfo === "elems" ? objType.inputs : objType.output; const highlighted = unifyFunctionTypes( - arr, - parsedQuery.elems, + typeInfo === "elems" ? objType.inputs : objType.output, + typeInfo === "elems" ? elems : returned, objType.where_clause, null, mgensOut => { @@ -2969,15 +2271,15 @@ class DocSearch { } }; - parsedQuery.elems.forEach(remapQuery); - parsedQuery.returned.forEach(remapQuery); + elems.forEach(remapQuery); + 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?: boolean}} fnType - input + * @param {{name: string|null, highlighted?: boolean}} fnType - input * @param {string[]} result */ const pushText = (fnType, result) => { @@ -3004,8 +2306,9 @@ class DocSearch { * * @param {rustdoc.HighlightedFunctionType} fnType - input * @param {string[]} result + * @returns {Promise<void>} */ - const writeHof = (fnType, result) => { + const writeHof = async(fnType, result) => { const hofOutput = fnType.bindings.get(this.typeNameIdOfOutput) || []; const hofInputs = fnType.generics; pushText(fnType, result); @@ -3016,7 +2319,7 @@ class DocSearch { pushText({ name: ", ", highlighted: false }, result); } needsComma = true; - writeFn(fnType, result); + await writeFn(fnType, result); } pushText({ name: hofOutput.length === 0 ? ")" : ") -> ", @@ -3031,7 +2334,7 @@ class DocSearch { pushText({ name: ", ", highlighted: false }, result); } needsComma = true; - writeFn(fnType, result); + await writeFn(fnType, result); } if (hofOutput.length > 1) { pushText({name: ")", highlighted: false}, result); @@ -3044,8 +2347,9 @@ class DocSearch { * * @param {rustdoc.HighlightedFunctionType} fnType * @param {string[]} result + * @returns {Promise<boolean>} */ - const writeSpecialPrimitive = (fnType, result) => { + const writeSpecialPrimitive = async(fnType, result) => { if (fnType.id === this.typeNameIdOfArray || fnType.id === this.typeNameIdOfSlice || fnType.id === this.typeNameIdOfTuple || fnType.id === this.typeNameIdOfUnit) { const [ob, sb] = @@ -3054,7 +2358,7 @@ class DocSearch { ["[", "]"] : ["(", ")"]; pushText({ name: ob, highlighted: fnType.highlighted }, result); - onEachBtwn( + await onEachBtwnAsync( fnType.generics, nested => writeFn(nested, result), // @ts-expect-error @@ -3065,11 +2369,11 @@ class DocSearch { } else if (fnType.id === this.typeNameIdOfReference) { pushText({ name: "&", highlighted: fnType.highlighted }, result); let prevHighlighted = false; - onEachBtwn( + await onEachBtwnAsync( fnType.generics, - value => { + async value => { prevHighlighted = !!value.highlighted; - writeFn(value, result); + await writeFn(value, result); }, // @ts-expect-error value => pushText({ @@ -3078,8 +2382,16 @@ class DocSearch { }, result), ); return true; - } else if (fnType.id === this.typeNameIdOfFn) { - writeHof(fnType, result); + } else if ( + fnType.id === this.typeNameIdOfFn || + fnType.id === this.typeNameIdOfFnMut || + fnType.id === this.typeNameIdOfFnOnce || + fnType.id === this.typeNameIdOfFnPtr + ) { + await writeHof(fnType, result); + return true; + } else if (fnType.id === this.typeNameIdOfNever) { + pushText({ name: "!", highlighted: fnType.highlighted }, result); return true; } return false; @@ -3091,8 +2403,9 @@ class DocSearch { * * @param {rustdoc.HighlightedFunctionType} fnType * @param {string[]} result + * @returns {Promise<void>} */ - const writeFn = (fnType, result) => { + const writeFn = async(fnType, result) => { if (fnType.id !== null && fnType.id < 0) { if (fnParamNames[-1 - fnType.id] === "") { // Normally, there's no need to shown an unhighlighted @@ -3101,7 +2414,7 @@ class DocSearch { fnType.generics : objType.where_clause[-1 - fnType.id]; for (const nested of generics) { - writeFn(nested, result); + await writeFn(nested, result); } return; } else if (mgens) { @@ -3120,7 +2433,7 @@ class DocSearch { }, result); /** @type{string[]} */ const where = []; - onEachBtwn( + await onEachBtwnAsync( fnType.generics, nested => writeFn(nested, where), // @ts-expect-error @@ -3131,32 +2444,61 @@ class DocSearch { } } else { if (fnType.ty === TY_PRIMITIVE) { - if (writeSpecialPrimitive(fnType, result)) { + if (await 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); + fnType.id === this.typeNameIdOfFnMut || + fnType.id === this.typeNameIdOfFnOnce || + fnType.id === this.typeNameIdOfFnPtr + )) { + await writeHof(fnType, result); + return; + } else if (fnType.name === "" && + fnType.bindings.size === 0 && + fnType.generics.length !== 0 + ) { + pushText({ name: "impl ", highlighted: false }, result); + if (fnType.generics.length > 1) { + pushText({ name: "(", highlighted: false }, result); + } + await onEachBtwnAsync( + fnType.generics, + value => writeFn(value, result), + // @ts-expect-error + () => pushText({ name: ", ", highlighted: false }, result), + ); + if (fnType.generics.length > 1) { + pushText({ name: ")", highlighted: false }, result); + } return; } pushText(fnType, result); let hasBindings = false; if (fnType.bindings.size > 0) { - onEachBtwn( - fnType.bindings, - ([key, values]) => { - const name = this.assocTypeIdNameMap.get(key); + await onEachBtwnAsync( + await Promise.all([...fnType.bindings.entries()].map( + /** + * @param {[number, rustdoc.HighlightedFunctionType[]]} param0 + * @returns {Promise<[ + * string|null, + * rustdoc.HighlightedFunctionType[], + * ]>} + */ + async([key, values]) => [await this.getName(key), values], + )), + async([name, values]) => { // @ts-expect-error if (values.length === 1 && values[0].id < 0 && // @ts-expect-error - `${fnType.name}::${name}` === fnParamNames[-1 - values[0].id]) { + `${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, []); + await writeFn(value, []); } return true; } @@ -3169,7 +2511,7 @@ class DocSearch { name: values.length !== 1 ? "=(" : "=", highlighted: false, }, result); - onEachBtwn( + await onEachBtwnAsync( values || [], value => writeFn(value, result), // @ts-expect-error @@ -3186,7 +2528,7 @@ class DocSearch { if (fnType.generics.length > 0) { pushText({ name: hasBindings ? ", " : "<", highlighted: false }, result); } - onEachBtwn( + await onEachBtwnAsync( fnType.generics, value => writeFn(value, result), // @ts-expect-error @@ -3199,14 +2541,14 @@ class DocSearch { }; /** @type {string[]} */ const type = []; - onEachBtwn( + await onEachBtwnAsync( fnInputs, fnType => writeFn(fnType, type), // @ts-expect-error () => pushText({ name: ", ", highlighted: false }, type), ); pushText({ name: " -> ", highlighted: false }, type); - onEachBtwn( + await onEachBtwnAsync( fnOutput, fnType => writeFn(fnType, type), // @ts-expect-error @@ -3216,170 +2558,196 @@ class DocSearch { 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. - * - * @param {rustdoc.Results} results - * @param {"sig"|"elems"|"returned"|null} typeInfo - * @param {string} preferredCrate - * @returns {Promise<rustdoc.ResultObject[]>} - */ - const sortResults = async(results, typeInfo, preferredCrate) => { - const userQuery = parsedQuery.userQuery; - const normalizedUserQuery = parsedQuery.userQuery.toLowerCase(); - const isMixedCase = normalizedUserQuery !== userQuery; - const result_list = []; - const isReturnTypeQuery = parsedQuery.elems.length === 0 || - typeInfo === "returned"; - for (const result of results.values()) { - result.item = this.searchIndex[result.id]; - result.word = this.searchIndex[result.id].word; - if (isReturnTypeQuery) { - // We are doing a return-type based search, deprioritize "clone-like" results, - // ie. functions that also take the queried type as an argument. - const resultItemType = result.item && result.item.type; - if (!resultItemType) { + const sortAndTransformResults = + /** + * @this {DocSearch} + * @param {Array<rustdoc.PlainResultObject|null>} results + * @param {"sig"|"elems"|"returned"|null} typeInfo + * @param {string} preferredCrate + * @param {Set<string>} duplicates + * @returns {AsyncGenerator<rustdoc.ResultObject, number>} + */ + async function*(results, typeInfo, preferredCrate, duplicates) { + const userQuery = parsedQuery.userQuery; + const normalizedUserQuery = parsedQuery.userQuery.toLowerCase(); + const isMixedCase = normalizedUserQuery !== userQuery; + const isReturnTypeQuery = parsedQuery.elems.length === 0 || + typeInfo === "returned"; + /** + * @type {[rustdoc.PlainResultObject, rustdoc.Row][]} + */ + const result_list = []; + for (const result of results.values()) { + if (!result) { + continue; + } + /** + * @type {rustdoc.Row?} + */ + const item = await this.getRow(result.id); + if (!item) { + continue; + } + if (filterCrates !== null && item.crate !== filterCrates) { continue; } - const inputs = resultItemType.inputs; - const where_clause = resultItemType.where_clause; - if (containsTypeFromQuery(inputs, where_clause)) { - result.path_dist *= 100; - result.dist *= 100; + if (isReturnTypeQuery && item.type && item.type.functionSignature) { + // we are doing a return-type based search, + // deprioritize "clone-like" results, + // ie. functions that also take the queried type as an argument. + const inputs = item.type.functionSignature.inputs; + const where_clause = item.type.functionSignature.where_clause; + if (containsTypeFromQuery( + result.elems, + result.returned, + inputs, + where_clause, + )) { + result.path_dist *= 100; + result.dist *= 100; + } + } + if (item) { + result_list.push([result, item]); + } else { + continue; } } - result_list.push(result); - } - result_list.sort((aaa, bbb) => { - /** @type {number} */ - let a; - /** @type {number} */ - let b; + result_list.sort(([aaa, aai], [bbb, bbi]) => { + /** @type {number} */ + let a; + /** @type {number} */ + let b; - // sort by exact case-sensitive match - if (isMixedCase) { - a = Number(aaa.item.name !== userQuery); - b = Number(bbb.item.name !== userQuery); - if (a !== b) { - return a - b; - } - } + if (typeInfo !== null) { + // in type based search... - // sort by exact match with regard to the last word (mismatch goes later) - a = Number(aaa.word !== normalizedUserQuery); - b = Number(bbb.word !== normalizedUserQuery); - if (a !== b) { - return a - b; - } + // put functions first + a = Number(!isFnLikeTy(aai.ty)); + b = Number(!isFnLikeTy(bbi.ty)); + if (a !== b) { + return a - b; + } + } else { + // in name based search... + + // sort by exact case-sensitive match + if (isMixedCase) { + a = Number(aai.name !== userQuery); + b = Number(bbi.name !== userQuery); + if (a !== b) { + return a - b; + } + } - // sort by index of keyword in item name (no literal occurrence goes later) - a = Number(aaa.index < 0); - b = Number(bbb.index < 0); - if (a !== b) { - return a - b; - } + a = Number(aaa.is_alias); + b = Number(bbb.is_alias); + if (a !== b) { + return b - a; + } - // in type based search, put functions first - if (parsedQuery.hasReturnArrow) { - a = Number(!isFnLikeTy(aaa.item.ty)); - b = Number(!isFnLikeTy(bbb.item.ty)); + // sort by exact match with regard to the last word (mismatch goes later) + a = Number(aai.normalizedName !== normalizedUserQuery); + b = Number(bbi.normalizedName !== normalizedUserQuery); + if (a !== b) { + return a - b; + } + + // sort by index of keyword in item name (no literal occurrence goes later) + a = Number(aaa.index < 0); + b = Number(bbb.index < 0); + if (a !== b) { + return a - b; + } + } + + // Sort by distance in the path part, if specified + // (less changes required to match means higher rankings) + a = Number(aaa.path_dist); + b = Number(bbb.path_dist); if (a !== b) { return a - b; } - } - - // Sort by distance in the path part, if specified - // (less changes required to match means higher rankings) - a = Number(aaa.path_dist); - b = Number(bbb.path_dist); - if (a !== b) { - return a - b; - } - // (later literal occurrence, if any, goes later) - a = Number(aaa.index); - b = Number(bbb.index); - if (a !== b) { - return a - b; - } + // (later literal occurrence, if any, goes later) + a = Number(aaa.index); + b = Number(bbb.index); + if (a !== b) { + return a - b; + } - // Sort by distance in the name part, the last part of the path - // (less changes required to match means higher rankings) - a = Number(aaa.dist); - b = Number(bbb.dist); - if (a !== b) { - return a - b; - } + // Sort by distance in the name part, the last part of the path + // (less changes required to match means higher rankings) + a = Number(aaa.dist); + b = Number(bbb.dist); + if (a !== b) { + return a - b; + } - // sort deprecated items later - a = Number( - // @ts-expect-error - this.searchIndexDeprecated.get(aaa.item.crate).contains(aaa.item.bitIndex), - ); - b = Number( - // @ts-expect-error - this.searchIndexDeprecated.get(bbb.item.crate).contains(bbb.item.bitIndex), - ); - if (a !== b) { - return a - b; - } + // sort deprecated items later + a = Number(aai.deprecated); + b = Number(bbi.deprecated); + if (a !== b) { + return a - b; + } - // sort by crate (current crate comes first) - a = Number(aaa.item.crate !== preferredCrate); - b = Number(bbb.item.crate !== preferredCrate); - if (a !== b) { - return a - b; - } + // sort by crate (current crate comes first) + a = Number(aai.crate !== preferredCrate); + b = Number(bbi.crate !== preferredCrate); + if (a !== b) { + return a - b; + } - // sort by item name length (longer goes later) - a = Number(aaa.word.length); - b = Number(bbb.word.length); - if (a !== b) { - return a - b; - } + // sort by item name length (longer goes later) + a = Number(aai.normalizedName.length); + b = Number(bbi.normalizedName.length); + if (a !== b) { + return a - b; + } - // sort by item name (lexicographically larger goes later) - let aw = aaa.word; - let bw = bbb.word; - if (aw !== bw) { - return (aw > bw ? +1 : -1); - } + // sort by item name (lexicographically larger goes later) + let aw = aai.normalizedName; + let bw = bbi.normalizedName; + if (aw !== bw) { + return (aw > bw ? +1 : -1); + } - // sort by description (no description goes later) - a = Number( - // @ts-expect-error - this.searchIndexEmptyDesc.get(aaa.item.crate).contains(aaa.item.bitIndex), - ); - b = Number( - // @ts-expect-error - this.searchIndexEmptyDesc.get(bbb.item.crate).contains(bbb.item.bitIndex), - ); - if (a !== b) { - return a - b; - } + // sort by description (no description goes later) + const di = this.database.getData("desc"); + if (di) { + a = Number(di.isEmpty(aaa.id)); + b = Number(di.isEmpty(bbb.id)); + if (a !== b) { + return a - b; + } + } - // sort by type (later occurrence in `itemTypes` goes later) - a = Number(aaa.item.ty); - b = Number(bbb.item.ty); - if (a !== b) { - return a - b; - } + // sort by type (later occurrence in `itemTypes` goes later) + a = Number(aai.ty); + b = Number(bbi.ty); + if (a !== b) { + return a - b; + } - // sort by path (lexicographically larger goes later) - aw = aaa.item.path; - bw = bbb.item.path; - if (aw !== bw) { - return (aw > bw ? +1 : -1); - } + // sort by path (lexicographically larger goes later) + const ap = aai.modulePath; + const bp = bbi.modulePath; + aw = ap === undefined ? "" : ap; + bw = bp === undefined ? "" : bp; + if (aw !== bw) { + return (aw > bw ? +1 : -1); + } - // que sera, sera - return 0; - }); + // que sera, sera + return 0; + }); - return transformResults(result_list, typeInfo); - }; + const transformed_result_list = transformResults(result_list, typeInfo, duplicates); + yield* transformed_result_list; + return transformed_result_list.length; + } + .bind(this); /** * This function checks if a list of search query `queryElems` can all be found in the @@ -3931,6 +3299,8 @@ class DocSearch { } return true; } else { + // For these special cases, matching code need added to the inverted index. + // search_index.rs -> convert_render_type does this if (queryElem.id === this.typeNameIdOfArrayOrSlice && (fnType.id === this.typeNameIdOfSlice || fnType.id === this.typeNameIdOfArray) ) { @@ -3941,10 +3311,12 @@ class DocSearch { ) { // () matches primitive:tuple or primitive:unit // if it matches, then we're fine, and this is an appropriate match candidate - } else if (queryElem.id === this.typeNameIdOfHof && - (fnType.id === this.typeNameIdOfFn || fnType.id === this.typeNameIdOfFnMut || - fnType.id === this.typeNameIdOfFnOnce) - ) { + } else if (queryElem.id === this.typeNameIdOfHof && ( + fnType.id === this.typeNameIdOfFn || + fnType.id === this.typeNameIdOfFnMut || + fnType.id === this.typeNameIdOfFnOnce || + fnType.id === this.typeNameIdOfFnPtr + )) { // -> matches fn, fnonce, and fnmut // if it matches, then we're fine, and this is an appropriate match candidate } else if (fnType.id !== queryElem.id || queryElem.id === null) { @@ -4127,12 +3499,14 @@ class DocSearch { * This function checks if the given list contains any * (non-generic) types mentioned in the query. * + * @param {rustdoc.QueryElement[]} elems + * @param {rustdoc.QueryElement[]} returned * @param {rustdoc.FunctionType[]} list - A list of function types. * @param {rustdoc.FunctionType[][]} where_clause - Trait bounds for generic items. */ - function containsTypeFromQuery(list, where_clause) { + function containsTypeFromQuery(elems, returned, list, where_clause) { if (!list) return false; - for (const ty of parsedQuery.returned) { + for (const ty of returned) { // negative type ids are generics if (ty.id !== null && ty.id < 0) { continue; @@ -4141,7 +3515,7 @@ class DocSearch { return true; } } - for (const ty of parsedQuery.elems) { + for (const ty of elems) { if (ty.id !== null && ty.id < 0) { continue; } @@ -4233,10 +3607,10 @@ class DocSearch { /** * Compute an "edit distance" that ignores missing path elements. * @param {string[]} contains search query path - * @param {rustdoc.Row} ty indexed item + * @param {string[]} path indexed page path * @returns {null|number} edit distance */ - function checkPath(contains, ty) { + function checkPath(contains, path) { if (contains.length === 0) { return 0; } @@ -4244,11 +3618,6 @@ class DocSearch { contains.reduce((acc, next) => acc + next.length, 0) / 3, ); let ret_dist = maxPathEditDistance + 1; - const path = ty.path.split("::"); - - if (ty.parent && ty.parent.name) { - path.push(ty.parent.name.toLowerCase()); - } const length = path.length; const clength = contains.length; @@ -4274,7 +3643,32 @@ class DocSearch { return ret_dist > maxPathEditDistance ? null : ret_dist; } - // @ts-expect-error + /** + * Compute an "edit distance" that ignores missing path elements. + * @param {string[]} contains search query path + * @param {rustdoc.Row} row indexed item + * @returns {null|number} edit distance + */ + function checkRowPath(contains, row) { + if (contains.length === 0) { + return 0; + } + + const path = row.modulePath.split("::"); + + if (row.parent && row.parent.name) { + path.push(row.parent.name.toLowerCase()); + } + + return checkPath(contains, path); + } + + /** + * + * @param {number} filter + * @param {rustdoc.ItemType} type + * @returns + */ function typePassesFilter(filter, type) { // No filter or Exact mach if (filter <= NO_TYPE_FILTER || filter === type) return true; @@ -4296,366 +3690,674 @@ class DocSearch { return false; } - // @ts-expect-error - const handleAliases = async(ret, query, filterCrates, currentCrate) => { - const lowerQuery = query.toLowerCase(); - if (this.FOUND_ALIASES.has(lowerQuery)) { - return; - } - this.FOUND_ALIASES.add(lowerQuery); - // We separate aliases and crate aliases because we want to have current crate - // aliases to be before the others in the displayed results. - // @ts-expect-error - const aliases = []; - // @ts-expect-error - const crateAliases = []; - if (filterCrates !== null) { - if (this.ALIASES.has(filterCrates) - && this.ALIASES.get(filterCrates).has(lowerQuery)) { - const query_aliases = this.ALIASES.get(filterCrates).get(lowerQuery); - for (const alias of query_aliases) { - aliases.push(alias); - } + const innerRunNameQuery = + /** + * @this {DocSearch} + * @param {string} currentCrate + * @returns {AsyncGenerator<rustdoc.ResultObject>} + */ + async function*(currentCrate) { + const index = this.database.getIndex("normalizedName"); + if (!index) { + return; } - } else { - for (const [crate, crateAliasesIndex] of this.ALIASES) { - if (crateAliasesIndex.has(lowerQuery)) { - // @ts-expect-error - const pushTo = crate === currentCrate ? crateAliases : aliases; - const query_aliases = crateAliasesIndex.get(lowerQuery); - for (const alias of query_aliases) { - pushTo.push(alias); + const idDuplicates = new Set(); + const pathDuplicates = new Set(); + let count = 0; + const prefixResults = []; + const normalizedUserQuery = parsedQuery.userQuery + .replace(/[_"]/g, "") + .toLowerCase(); + /** + * @param {string} name + * @param {number} alias + * @returns {Promise<rustdoc.PlainResultObject?>} + */ + const handleAlias = async(name, alias) => { + return { + id: alias, + dist: 0, + path_dist: 0, + index: 0, + alias: name, + is_alias: true, + elems: [], // only used in type-based queries + returned: [], // only used in type-based queries + original: await this.getRow(alias), + }; + }; + /** + * @param {Promise<rustdoc.PlainResultObject|null>[]} data + * @returns {AsyncGenerator<rustdoc.ResultObject, boolean>} + */ + const flush = async function* (data) { + const satr = sortAndTransformResults( + await Promise.all(data), + null, + currentCrate, + pathDuplicates, + ); + data.length = 0; + for await (const processed of satr) { + yield processed; + count += 1; + if ((count & 0x7F) === 0) { + await yieldToBrowser(); + } + if (count >= MAX_RESULTS) { + return true; } } - } - } - - // @ts-expect-error - const sortFunc = (aaa, bbb) => { - if (aaa.original.path < bbb.original.path) { - return 1; - } else if (aaa.original.path === bbb.original.path) { - return 0; - } - return -1; - }; - // @ts-expect-error - crateAliases.sort(sortFunc); - aliases.sort(sortFunc); - - // @ts-expect-error - const pushFunc = alias => { - // Cloning `alias` to prevent its fields to be updated. - alias = {...alias}; - const res = buildHrefAndPath(alias); - alias.displayPath = pathSplitter(res[0]); - alias.fullPath = alias.displayPath + alias.name; - alias.href = res[1]; - - ret.others.unshift(alias); - if (ret.others.length > MAX_RESULTS) { - ret.others.pop(); - } - }; - - aliases.forEach(pushFunc); - // @ts-expect-error - crateAliases.forEach(pushFunc); - }; - - /** - * This function adds the given result into the provided `results` map if it matches the - * following condition: - * - * * If it is a "literal search" (`parsedQuery.literalSearch`), then `dist` must be 0. - * * If it is not a "literal search", `dist` must be <= `maxEditDistance`. - * - * The `results` map contains information which will be used to sort the search results: - * - * * `fullId` is an `integer`` used as the key of the object we use for the `results` map. - * * `id` is the index in the `searchIndex` array for this element. - * * `index` is an `integer`` used to sort by the position of the word in the item's name. - * * `dist` is the main metric used to sort the search results. - * * `path_dist` is zero if a single-component search query is used, otherwise it's the - * distance computed for everything other than the last path component. - * - * @param {rustdoc.Results} results - * @param {number} fullId - * @param {number} id - * @param {number} index - * @param {number} dist - * @param {number} path_dist - * @param {number} maxEditDistance - */ - function addIntoResults(results, fullId, id, index, dist, path_dist, maxEditDistance) { - if (dist <= maxEditDistance || index !== -1) { - if (results.has(fullId)) { - const result = results.get(fullId); - if (result === undefined || result.dontValidate || result.dist <= dist) { - return; + return false; + }; + const aliasResults = await index.search(normalizedUserQuery); + if (aliasResults) { + for (const id of aliasResults.matches().entries()) { + const [name, alias] = await Promise.all([ + this.getName(id), + this.getAliasPointer(id), + ]); + // the index stores normalized names, but aliases are matched by exact names + if (name !== null && + alias !== null && + !idDuplicates.has(id) && + name.replace(/[_"]/g, "").toLowerCase() === normalizedUserQuery + ) { + prefixResults.push(handleAlias(name, alias)); + idDuplicates.add(id); + } } } - // @ts-expect-error - results.set(fullId, { - id: id, - index: index, - dontValidate: parsedQuery.literalSearch, - dist: dist, - path_dist: path_dist, - }); - } - } - - /** - * This function is called in case the query has more than one element. In this case, it'll - * try to match the items which validates all the elements. For `aa -> bb` will look for - * functions which have a parameter `aa` and has `bb` in its returned values. - * - * @param {rustdoc.Row} row - * @param {number} pos - Position in the `searchIndex`. - * @param {rustdoc.Results} results - */ - function handleArgs(row, pos, results) { - if (!row || (filterCrates !== null && row.crate !== filterCrates)) { - return; - } - const rowType = row.type; - if (!rowType) { - return; - } - - const tfpDist = compareTypeFingerprints( - row.id, - parsedQuery.typeFingerprint, - ); - if (tfpDist === null) { - return; - } - // @ts-expect-error - if (results.size >= MAX_RESULTS && tfpDist > results.max_dist) { - return; - } - - // If the result is too "bad", we return false and it ends this search. - if (!unifyFunctionTypes( - rowType.inputs, - parsedQuery.elems, - rowType.where_clause, - null, - // @ts-expect-error - mgens => { - return unifyFunctionTypes( - rowType.output, - parsedQuery.returned, - rowType.where_clause, - mgens, - checkTypeMgensForConflict, - 0, // unboxing depth - ); - }, - 0, // unboxing depth - )) { - return; - } - - results.max_dist = Math.max(results.max_dist || 0, tfpDist); - addIntoResults(results, row.id, pos, 0, tfpDist, 0, Number.MAX_VALUE); - } - - /** - * Compare the query fingerprint with the function fingerprint. - * - * @param {number} fullId - The function - * @param {Uint32Array} queryFingerprint - The query - * @returns {number|null} - Null if non-match, number if distance - * This function might return 0! - */ - const compareTypeFingerprints = (fullId, queryFingerprint) => { - const fh0 = this.functionTypeFingerprint[fullId * 4]; - const fh1 = this.functionTypeFingerprint[(fullId * 4) + 1]; - const fh2 = this.functionTypeFingerprint[(fullId * 4) + 2]; - const [qh0, qh1, qh2] = queryFingerprint; - // Approximate set intersection with bloom filters. - // This can be larger than reality, not smaller, because hashes have - // the property that if they've got the same value, they hash to the - // same thing. False positives exist, but not false negatives. - const [in0, in1, in2] = [fh0 & qh0, fh1 & qh1, fh2 & qh2]; - // Approximate the set of items in the query but not the function. - // This might be smaller than reality, but cannot be bigger. - // - // | in_ | qh_ | XOR | Meaning | - // | --- | --- | --- | ------------------------------------------------ | - // | 0 | 0 | 0 | Not present | - // | 1 | 0 | 1 | IMPOSSIBLE because `in_` is `fh_ & qh_` | - // | 1 | 1 | 0 | If one or both is false positive, false negative | - // | 0 | 1 | 1 | Since in_ has no false negatives, must be real | - if ((in0 ^ qh0) || (in1 ^ qh1) || (in2 ^ qh2)) { - return null; - } - return this.functionTypeFingerprint[(fullId * 4) + 3]; - }; - - - const innerRunQuery = () => { - if (parsedQuery.foundElems === 1 && !parsedQuery.hasReturnArrow) { + if (parsedQuery.error !== null || parsedQuery.elems.length === 0) { + yield* flush(prefixResults); + return; + } const elem = parsedQuery.elems[0]; - // use arrow functions to preserve `this`. - /** @type {function(number): void} */ - const handleNameSearch = id => { - const row = this.searchIndex[id]; - if (!typePassesFilter(elem.typeFilter, row.ty) || + const typeFilter = itemTypeFromName(elem.typeFilter); + /** + * @param {number} id + * @returns {Promise<rustdoc.PlainResultObject?>} + */ + const handleNameSearch = async id => { + const row = await this.getRow(id); + if (!row || !row.entry) { + return null; + } + if (!typePassesFilter(typeFilter, row.ty) || (filterCrates !== null && row.crate !== filterCrates)) { - return; + return null; } + /** @type {number|null} */ let pathDist = 0; if (elem.fullPath.length > 1) { - - const maybePathDist = checkPath(elem.pathWithoutLast, row); - if (maybePathDist === null) { - return; + pathDist = checkRowPath(elem.pathWithoutLast, row); + if (pathDist === null) { + return null; } - pathDist = maybePathDist; } if (parsedQuery.literalSearch) { - if (row.word === elem.pathLast) { - addIntoResults(results_others, row.id, id, 0, 0, pathDist, 0); - } + return row.name.toLowerCase() === elem.pathLast ? { + id, + dist: 0, + path_dist: 0, + index: 0, + elems: [], // only used in type-based queries + returned: [], // only used in type-based queries + is_alias: false, + } : null; } else { - addIntoResults( - results_others, - row.id, + return { id, - row.normalizedName.indexOf(elem.normalizedPathLast), - editDistance( + dist: editDistance( row.normalizedName, elem.normalizedPathLast, maxEditDistance, ), - pathDist, - maxEditDistance, - ); + path_dist: pathDist, + index: row.normalizedName.indexOf(elem.normalizedPathLast), + elems: [], // only used in type-based queries + returned: [], // only used in type-based queries + is_alias: false, + }; } }; - if (elem.normalizedPathLast !== "") { - const last = elem.normalizedPathLast; - for (const id of this.nameTrie.search(last, this.tailTable)) { - handleNameSearch(id); + if (elem.normalizedPathLast === "") { + // faster full-table scan for this specific case. + const nameData = this.database.getData("name"); + const l = nameData ? nameData.length : 0; + for (let id = 0; id < l; ++id) { + if (!idDuplicates.has(id)) { + idDuplicates.add(id); + prefixResults.push(handleNameSearch(id)); + } + if (yield* flush(prefixResults)) { + return; + } + } + return; + } + const results = await index.search(elem.normalizedPathLast); + if (results) { + for await (const result of results.prefixMatches()) { + for (const id of result.entries()) { + if (!idDuplicates.has(id)) { + idDuplicates.add(id); + prefixResults.push(handleNameSearch(id)); + const [name, alias] = await Promise.all([ + this.getName(id), + this.getAliasPointer(id), + ]); + if (name !== null && alias !== null) { + prefixResults.push(handleAlias(name, alias)); + } + } + } + if (prefixResults.length > MAX_RESULTS && (yield* flush(prefixResults))) { + return; + } + } + if (yield* flush(prefixResults)) { + return; + } + } + const levSearchResults = index.searchLev(elem.normalizedPathLast); + const levResults = []; + for await (const levResult of levSearchResults) { + for (const id of levResult.matches().entries()) { + if (!idDuplicates.has(id)) { + idDuplicates.add(id); + levResults.push(handleNameSearch(id)); + const [name, alias] = await Promise.all([ + this.getName(id), + this.getAliasPointer(id), + ]); + if (name !== null && alias !== null) { + levResults.push(handleAlias(name, alias)); + } + } + } + } + yield* flush(levResults); + if (results) { + const substringResults = []; + for await (const result of results.substringMatches()) { + for (const id of result.entries()) { + if (!idDuplicates.has(id)) { + idDuplicates.add(id); + substringResults.push(handleNameSearch(id)); + const [name, alias] = await Promise.all([ + this.getName(id), + this.getAliasPointer(id), + ]); + if (name !== null && alias !== null) { + substringResults.push(handleAlias(name, alias)); + } + } + } + if (yield* flush(substringResults)) { + return; + } } } - const length = this.searchIndex.length; + } + .bind(this); - for (let i = 0, nSearchIndex = length; i < nSearchIndex; ++i) { - // queries that end in :: bypass the trie - if (elem.normalizedPathLast === "") { - handleNameSearch(i); + const innerRunTypeQuery = + /** + * @this {DocSearch} + * @param {rustdoc.ParserQueryElement[]} inputs + * @param {rustdoc.ParserQueryElement[]} output + * @param {"sig"|"elems"|"returned"|null} typeInfo + * @param {string} currentCrate + * @returns {AsyncGenerator<rustdoc.ResultObject>} + */ + async function*(inputs, output, typeInfo, currentCrate) { + const index = this.database.getIndex("normalizedName"); + if (!index) { + return; + } + /** @type {Map<string, number>} */ + const genericMap = new Map(); + /** @type {[stringdex.RoaringBitmap, rustdoc.QueryElement][]} */ + const empty_postings_list = []; + /** + * Fetch a bitmap of potentially-matching functions, + * plus a list of query elements annotated with the correct IDs. + * + * More than one ID can exist because, for example, q=`Iter` can match + * `std::vec::Iter`, or `std::btree_set::Iter`, or anything else, and those + * items different IDs. What's worse, q=`Iter<Iter>` has N**2 possible + * matches, because it could be `vec::Iter<btree_set::Iter>`, + * `btree_set::Iter<vec::Iter>`, `vec::Iter<vec::Iter>`, + * `btree_set::Iter<btree_set::Iter>`, + * or anything else. This function returns all possible permutations. + * + * @param {rustdoc.ParserQueryElement|null} elem + * @returns {Promise<[stringdex.RoaringBitmap, rustdoc.QueryElement][]>} + */ + const unpackPostingsList = async elem => { + if (!elem) { + return empty_postings_list; + } + const typeFilter = itemTypeFromName(elem.typeFilter); + const searchResults = await index.search(elem.normalizedPathLast); + /** + * @type {Promise<[ + * number, + * string|null, + * rustdoc.TypeData|null, + * rustdoc.PathData|null, + * ]>[]} + * */ + const typePromises = []; + if (typeFilter !== TY_GENERIC && searchResults) { + for (const id of searchResults.matches().entries()) { + typePromises.push(Promise.all([ + this.getName(id), + this.getTypeData(id), + this.getPathData(id), + ]).then(([name, typeData, pathData]) => + [id, name, typeData, pathData])); + } + } + const types = (await Promise.all(typePromises)) + .filter(([_id, name, ty, path]) => + name !== null && name.toLowerCase() === elem.pathLast && + ty && !ty.invertedFunctionSignatureIndex.isEmpty() && + path && path.ty !== TY_ASSOCTYPE && + (elem.pathWithoutLast.length === 0 || + checkPath( + elem.pathWithoutLast, + path.modulePath.split("::"), + ) === 0), + ); + if (types.length === 0) { + const areGenericsAllowed = typeFilter === TY_GENERIC || ( + typeFilter === -1 && + (parsedQuery.totalElems > 1 || parsedQuery.hasReturnArrow) && + elem.pathWithoutLast.length === 0 && + elem.generics.length === 0 && + elem.bindings.size === 0 + ); + if (typeFilter !== TY_GENERIC && + (elem.name.length >= 3 || !areGenericsAllowed) + ) { + /** @type {string|null} */ + let chosenName = null; + /** @type {rustdoc.TypeData[]} */ + let chosenType = []; + /** @type {rustdoc.PathData[]} */ + let chosenPath = []; + /** @type {number[]} */ + let chosenId = []; + let chosenDist = Number.MAX_SAFE_INTEGER; + const levResults = index.searchLev(elem.normalizedPathLast); + for await (const searchResults of levResults) { + for (const id of searchResults.matches().entries()) { + const [name, ty, path] = await Promise.all([ + this.getName(id), + this.getTypeData(id), + this.getPathData(id), + ]); + if (name !== null && ty !== null && path !== null && + !ty.invertedFunctionSignatureIndex.isEmpty() && + path.ty !== TY_ASSOCTYPE + ) { + let dist = editDistance( + name, + elem.pathLast, + maxEditDistance, + ); + if (elem.pathWithoutLast.length !== 0) { + const pathDist = checkPath( + elem.pathWithoutLast, + path.modulePath.split("::"), + ); + // guaranteed to be higher than the path limit + dist += pathDist === null ? + Number.MAX_SAFE_INTEGER : + pathDist; + } + if (name === chosenName) { + chosenId.push(id); + chosenType.push(ty); + chosenPath.push(path); + } else if (dist < chosenDist) { + chosenName = name; + chosenId = [id]; + chosenType = [ty]; + chosenPath = [path]; + chosenDist = dist; + } + } + } + if (chosenId.length !== 0) { + // searchLev returns results in order + // if we have working matches, we're done + break; + } + } + if (areGenericsAllowed) { + parsedQuery.proposeCorrectionFrom = elem.name; + parsedQuery.proposeCorrectionTo = chosenName; + } else { + parsedQuery.correction = chosenName; + for (let i = 0; i < chosenType.length; ++i) { + types.push([ + chosenId[i], + chosenName, + chosenType[i], + chosenPath[i], + ]); + } + } + } + if (areGenericsAllowed) { + let genericId = genericMap.get(elem.normalizedPathLast); + if (genericId === undefined) { + genericId = genericMap.size; + genericMap.set(elem.normalizedPathLast, genericId); + } + return [[ + await this.getGenericInvertedIndex(genericId), + { + name: elem.name, + id: (-genericId) - 1, + typeFilter: TY_GENERIC, + generics: [], + bindings: EMPTY_BINDINGS_MAP, + fullPath: elem.fullPath, + pathLast: elem.pathLast, + normalizedPathLast: elem.normalizedPathLast, + pathWithoutLast: elem.pathWithoutLast, + }, + ]]; + } + } + types.sort(([_i, name1, _t, pathData1], [_i2, name2, _t2, pathData2]) => { + const p1 = !pathData1 ? "" : pathData1.modulePath; + const p2 = !pathData2 ? "" : pathData2.modulePath; + const n1 = name1 === null ? "" : name1; + const n2 = name2 === null ? "" : name2; + if (p1.length !== p2.length) { + return p1.length > p2.length ? +1 : -1; + } + if (n1.length !== n2.length) { + return n1.length > n2.length ? +1 : -1; + } + if (n1 !== n2) { + return n1 > n2 ? +1 : -1; + } + if (p1 !== p2) { + return p1 > p2 ? +1 : -1; + } + return 0; + }); + /** @type {[stringdex.RoaringBitmap, rustdoc.QueryElement][]} */ + const results = []; + for (const [id, _name, typeData] of types) { + if (!typeData || typeData.invertedFunctionSignatureIndex.isEmpty()) { + continue; + } + const upla = await unpackPostingsListAll(elem.generics); + const uplb = await unpackPostingsListBindings(elem.bindings); + for (const [genericsPostingList, generics] of upla) { + for (const [bindingsPostingList, bindings] of uplb) { + results.push([ + typeData.invertedFunctionSignatureIndex + .intersection(bindingsPostingList) + .intersection(genericsPostingList), + { + name: elem.name, + id, + typeFilter, + generics, + bindings, + fullPath: elem.fullPath, + pathLast: elem.pathLast, + normalizedPathLast: elem.normalizedPathLast, + pathWithoutLast: elem.pathWithoutLast, + }, + ]); + if ((results.length & 0x7F) === 0) { + await yieldToBrowser(); + } + } + } + } + return results; + }; + /** @type {[stringdex.RoaringBitmap, rustdoc.QueryElement[]][]} */ + const empty_nested_postings_list = [[RoaringBitmap.everything(), []]]; + /** + * Fetch all possible matching permutations of a list of query elements. + * + * The empty list returns an "identity postings list", with a bitmap that + * matches everything and an empty list of elems. This allows you to safely + * take the intersection of this bitmap. + * + * @param {(rustdoc.ParserQueryElement|null)[]|null} elems + * @returns {Promise<[stringdex.RoaringBitmap, rustdoc.QueryElement[]][]>} + */ + const unpackPostingsListAll = async elems => { + if (!elems || elems.length === 0) { + return empty_nested_postings_list; + } + const [firstPostingsList, remainingAll] = await Promise.all([ + unpackPostingsList(elems[0]), + unpackPostingsListAll(elems.slice(1)), + ]); + /** @type {[stringdex.RoaringBitmap, rustdoc.QueryElement[]][]} */ + const results = []; + for (const [firstBitmap, firstElem] of firstPostingsList) { + for (const [remainingBitmap, remainingElems] of remainingAll) { + results.push([ + firstBitmap.intersection(remainingBitmap), + [firstElem, ...remainingElems], + ]); + if ((results.length & 0x7F) === 0) { + await yieldToBrowser(); + } + } + } + return results; + }; + /** + * Fetch all possible matching permutations of a map query element bindings. + * + * The empty list returns an "identity postings list", with a bitmap that + * matches everything and an empty list of elems. This allows you to safely + * take the intersection of this bitmap. + * + * Heads up! This function mutates the Map that you provide. + * Before passing an actual parser item to it, make sure to clone the map. + * + * @param {Map<string, rustdoc.ParserQueryElement[]>} elems + * @returns {Promise<[ + * stringdex.RoaringBitmap, + * Map<number, rustdoc.QueryElement[]>, + * ][]>} + */ + const unpackPostingsListBindings = async elems => { + if (!elems) { + return [[RoaringBitmap.everything(), new Map()]]; + } + const firstKey = elems.keys().next().value; + if (firstKey === undefined) { + return [[RoaringBitmap.everything(), new Map()]]; + } + const firstList = elems.get(firstKey); + if (firstList === undefined) { + return [[RoaringBitmap.everything(), new Map()]]; } - const row = this.searchIndex[i]; - if (filterCrates !== null && row.crate !== filterCrates) { - continue; + const firstKeyIds = await index.search(firstKey); + if (!firstKeyIds) { + // User specified a non-existent key. + return [[RoaringBitmap.empty(), new Map()]]; } - const tfpDist = compareTypeFingerprints( - row.id, - parsedQuery.typeFingerprint, - ); - if (tfpDist !== null) { - const in_args = row.type && row.type.inputs - && checkIfInList(row.type.inputs, elem, row.type.where_clause, null, 0); - const returned = row.type && row.type.output - && checkIfInList(row.type.output, elem, row.type.where_clause, null, 0); - if (in_args) { - results_in_args.max_dist = Math.max( - results_in_args.max_dist || 0, - tfpDist, - ); - const maxDist = results_in_args.size < MAX_RESULTS ? - (tfpDist + 1) : - results_in_args.max_dist; - addIntoResults(results_in_args, row.id, i, -1, tfpDist, 0, maxDist); - } - if (returned) { - results_returned.max_dist = Math.max( - results_returned.max_dist || 0, - tfpDist, - ); - const maxDist = results_returned.size < MAX_RESULTS ? - (tfpDist + 1) : - results_returned.max_dist; - addIntoResults(results_returned, row.id, i, -1, tfpDist, 0, maxDist); + elems.delete(firstKey); + const [firstPostingsList, remainingAll] = await Promise.all([ + unpackPostingsListAll(firstList), + unpackPostingsListBindings(elems), + ]); + /** @type {[stringdex.RoaringBitmap, Map<number, rustdoc.QueryElement[]>][]} */ + const results = []; + for (const keyId of firstKeyIds.matches().entries()) { + for (const [firstBitmap, firstElem] of firstPostingsList) { + for (const [remainingBitmap, remainingElems] of remainingAll) { + const elems = new Map(remainingElems); + elems.set(keyId, firstElem); + results.push([ + firstBitmap.intersection(remainingBitmap), + elems, + ]); + if ((results.length & 0x7F) === 0) { + await yieldToBrowser(); + } + } } } - } - } else if (parsedQuery.foundElems > 0) { - // Sort input and output so that generic type variables go first and - // types with generic parameters go last. - // That's because of the way unification is structured: it eats off - // the end, and hits a fast path if the last item is a simple atom. - /** @type {function(rustdoc.QueryElement, rustdoc.QueryElement): number} */ - const sortQ = (a, b) => { - const ag = a.generics.length === 0 && a.bindings.size === 0; - const bg = b.generics.length === 0 && b.bindings.size === 0; - if (ag !== bg) { - // unary `+` converts booleans into integers. - return +ag - +bg; + elems.set(firstKey, firstList); + if (results.length === 0) { + // User specified a non-existent key. + return [[RoaringBitmap.empty(), new Map()]]; } - const ai = a.id !== null && a.id > 0; - const bi = b.id !== null && b.id > 0; - return +ai - +bi; + return results; }; - parsedQuery.elems.sort(sortQ); - parsedQuery.returned.sort(sortQ); - for (let i = 0, nSearchIndex = this.searchIndex.length; i < nSearchIndex; ++i) { - handleArgs(this.searchIndex[i], i, results_others); - } - } - }; - - if (parsedQuery.error === null) { - innerRunQuery(); - } - const isType = parsedQuery.foundElems !== 1 || parsedQuery.hasReturnArrow; - const [sorted_in_args, sorted_returned, sorted_others] = await Promise.all([ - sortResults(results_in_args, "elems", currentCrate), - sortResults(results_returned, "returned", currentCrate), - // @ts-expect-error - sortResults(results_others, (isType ? "query" : null), currentCrate), - ]); - const ret = createQueryResults( - sorted_in_args, - sorted_returned, - sorted_others, - parsedQuery); - await handleAliases(ret, parsedQuery.userQuery.replace(/"/g, ""), - filterCrates, currentCrate); - await Promise.all([ret.others, ret.returned, ret.in_args].map(async list => { - const descs = await Promise.all(list.map(result => { - // @ts-expect-error - return this.searchIndexEmptyDesc.get(result.crate).contains(result.bitIndex) ? - "" : - // @ts-expect-error - this.searchState.loadDesc(result); - })); - for (const [i, result] of list.entries()) { - // @ts-expect-error - result.desc = descs[i]; + // finally, we can do the actual unification loop + const [allInputs, allOutput] = await Promise.all([ + unpackPostingsListAll(inputs), + unpackPostingsListAll(output), + ]); + const results = []; + let checkCounter = 0; + let maxDist = 0; + for (const [inputsPostingList, inputs] of allInputs) { + for (const [outputPostingList, output] of allOutput) { + const postingList = inputsPostingList.intersection(outputPostingList); + for (const id of postingList.entries()) { + checkCounter += 1; + if ((checkCounter & 0x7F) === 0) { + await yieldToBrowser(); + } + const fnData = await this.getFunctionData(id); + if (!fnData || !fnData.functionSignature) { + continue; + } + if (fnData.elemCount > maxDist) { + if (results.length > 200) { + continue; + } else { + maxDist = fnData.elemCount; + } + } + const functionSignature = fnData.functionSignature; + if (!unifyFunctionTypes( + functionSignature.inputs, + inputs, + functionSignature.where_clause, + null, + mgens => { + return !!unifyFunctionTypes( + functionSignature.output, + output, + functionSignature.where_clause, + mgens, + checkTypeMgensForConflict, + 0, // unboxing depth + ); + }, + 0, // unboxing depth + )) { + continue; + } + results.push({ + id, + dist: fnData.elemCount, + path_dist: 0, + index: -1, + elems: inputs, + returned: output, + is_alias: false, + }); + } + } + } + yield* sortAndTransformResults( + results, + typeInfo, + currentCrate, + new Set(), + ); } - })); - if (parsedQuery.error !== null && ret.others.length !== 0) { - // It means some doc aliases were found so let's "remove" the error! - ret.query.error = null; + .bind(this); + + if (parsedQuery.foundElems === 1 && !parsedQuery.hasReturnArrow) { + // We never want the main tab to delay behind the other two tabs. + // This is a bit of a hack (because JS's scheduler doesn't have much of an API), + // along with making innerRunTypeQuery yield to the UI thread. + const { + promise: donePromise, + resolve: doneResolve, + reject: doneReject, + } = Promise.withResolvers(); + const doneTimeout = timeout(250); + return { + "in_args": (async function*() { + await Promise.race([donePromise, doneTimeout]); + yield* innerRunTypeQuery(parsedQuery.elems, [], "elems", currentCrate); + })(), + "returned": (async function*() { + await Promise.race([donePromise, doneTimeout]); + yield* innerRunTypeQuery([], parsedQuery.elems, "returned", currentCrate); + })(), + "others": (async function*() { + try { + yield* innerRunNameQuery(currentCrate); + doneResolve(null); + } catch (e) { + doneReject(e); + throw e; + } + })(), + "query": parsedQuery, + }; + } else if (parsedQuery.error !== null) { + return { + "in_args": (async function*() {})(), + "returned": (async function*() {})(), + "others": innerRunNameQuery(currentCrate), + "query": parsedQuery, + }; + } else { + const typeInfo = parsedQuery.elems.length === 0 ? + "returned" : ( + parsedQuery.returned.length === 0 ? "elems" : "sig" + ); + return { + "in_args": (async function*() {})(), + "returned": (async function*() {})(), + "others": parsedQuery.foundElems === 0 ? + (async function*() {})() : + innerRunTypeQuery( + parsedQuery.elems, + parsedQuery.returned, + typeInfo, + currentCrate, + ), + "query": parsedQuery, + }; } - return ret; } } // ==================== Core search logic end ==================== -/** @type {Map<string, rustdoc.RawSearchIndexCrate>} */ -let rawSearchIndex; -// @ts-expect-error +/** @type {DocSearch} */ let docSearch; const longItemTypes = [ "keyword", @@ -4755,12 +4457,8 @@ function buildUrl(search, filterCrates) { function getFilterCrates() { const elem = document.getElementById("crate-search"); - if (elem && - // @ts-expect-error - elem.value !== "all crates" && - // @ts-expect-error - window.searchIndex.has(elem.value) - ) { + // @ts-expect-error + if (elem && elem.value !== "all crates") { // @ts-expect-error return elem.value; } @@ -4770,8 +4468,7 @@ function getFilterCrates() { // @ts-expect-error function nextTab(direction) { const next = (searchState.currentTab + direction + 3) % searchState.focusedByTab.length; - // @ts-expect-error - searchState.focusedByTab[searchState.currentTab] = document.activeElement; + window.searchState.focusedByTab[searchState.currentTab] = document.activeElement; printTab(next); focusSearchResult(); } @@ -4783,133 +4480,182 @@ function focusSearchResult() { document.querySelectorAll(".search-results.active a").item(0) || document.querySelectorAll("#search-tabs button").item(searchState.currentTab); searchState.focusedByTab[searchState.currentTab] = null; - if (target) { - // @ts-expect-error + if (target && target instanceof HTMLElement) { target.focus(); } } /** * Render a set of search results for a single tab. - * @param {Array<?>} array - The search results for this tab - * @param {rustdoc.ParsedQuery<rustdoc.QueryElement>} query + * @param {AsyncGenerator<rustdoc.ResultObject>} results - The search results for this tab + * @param {rustdoc.ParsedQuery<rustdoc.ParserQueryElement>} query * @param {boolean} display - True if this is the active tab + * @param {function(number, HTMLElement): any} finishedCallback + * @param {boolean} isTypeSearch + * @returns {Promise<HTMLElement>} */ -async function addTab(array, query, display) { +async function addTab(results, query, display, finishedCallback, isTypeSearch) { const extraClass = display ? " active" : ""; - const output = document.createElement( - array.length === 0 && query.error === null ? "div" : "ul", - ); - if (array.length > 0) { - output.className = "search-results " + extraClass; + /** @type {HTMLElement} */ + let output = document.createElement("ul"); + output.className = "search-results " + extraClass; - const lis = Promise.all(array.map(async item => { - const name = item.is_alias ? item.original.name : item.name; - const type = itemTypes[item.ty]; - const longType = longItemTypes[item.ty]; - const typeName = longType.length !== 0 ? `${longType}` : "?"; + let count = 0; + + /** @type {Promise<string|null>[]} */ + const descList = []; + + /** @param {rustdoc.ResultObject} obj */ + const addNextResultToOutput = async obj => { + count += 1; - const link = document.createElement("a"); - link.className = "result-" + type; - link.href = item.href; + const name = obj.item.name; + const type = itemTypes[obj.item.ty]; + const longType = longItemTypes[obj.item.ty]; + const typeName = longType.length !== 0 ? `${longType}` : "?"; - const resultName = document.createElement("span"); - resultName.className = "result-name"; + const link = document.createElement("a"); + link.className = "result-" + type; + link.href = obj.href; - resultName.insertAdjacentHTML( - "beforeend", - `<span class="typename">${typeName}</span>`); - link.appendChild(resultName); + const resultName = document.createElement("span"); + resultName.className = "result-name"; - let alias = " "; - if (item.is_alias) { - alias = ` <div class="alias">\ -<b>${item.name}</b><i class="grey"> - see </i>\ + resultName.insertAdjacentHTML( + "beforeend", + `<span class="typename">${typeName}</span>`); + link.appendChild(resultName); + + let alias = " "; + if (obj.alias !== undefined) { + alias = ` <div class="alias">\ +<b>${obj.alias}</b><i class="grey"> - see </i>\ </div>`; - } - resultName.insertAdjacentHTML( - "beforeend", - `<div class="path">${alias}\ -${item.displayPath}<span class="${type}">${name}</span>\ + } + resultName.insertAdjacentHTML( + "beforeend", + `<div class="path">${alias}\ +${obj.displayPath}<span class="${type}">${name}</span>\ </div>`); - 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"); - // @ts-expect-error - 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)); + const description = document.createElement("div"); + description.className = "desc"; + obj.desc.then(desc => { + if (desc !== null) { + description.insertAdjacentHTML("beforeend", desc); + } + }); + descList.push(obj.desc); + if (obj.displayTypeSignature) { + const {type, mappedNames, whereClause} = await obj.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 [qname, name] of mappedNames) { + // don't care unless the generic name is different + if (name === qname) { + continue; } - }); - 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 [qname, name] 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); + 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; } - 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)); } - addWhereLineFn(); - const line = document.createElement("div"); - line.className = "where"; - line.appendChild(document.createTextNode(` ${name}: `)); - // @ts-expect-error - 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.appendChild(line); } - displayType.className = "type-signature"; - link.appendChild(displayType); } + displayType.className = "type-signature"; + link.appendChild(displayType); + } + + link.appendChild(description); + output.appendChild(link); - link.appendChild(description); - return link; - })); - lis.then(lis => { - for (const li of lis) { - output.appendChild(li); + results.next().then(async nextResult => { + if (nextResult.value) { + addNextResultToOutput(nextResult.value); + } else { + await Promise.all(descList); + // need to make sure the element is shown before + // running this callback + yieldToBrowser().then(() => finishedCallback(count, output)); } }); - } else if (query.error === null) { - const dlroChannel = `https://doc.rust-lang.org/${getVar("channel")}`; + }; + const firstResult = await results.next(); + let correctionOutput = ""; + if (query.correction !== null && isTypeSearch) { + const orig = query.returned.length > 0 + ? query.returned[0].name + : query.elems[0].name; + correctionOutput = "<h3 class=\"search-corrections\">" + + `Type "${orig}" not found. ` + + "Showing results for closest type name " + + `"${query.correction}" instead.</h3>`; + } + if (query.proposeCorrectionFrom !== null && isTypeSearch) { + const orig = query.proposeCorrectionFrom; + const targ = query.proposeCorrectionTo; + correctionOutput = "<h3 class=\"search-corrections\">" + + `Type "${orig}" not found and used as generic parameter. ` + + `Consider searching for "${targ}" instead.</h3>`; + } + if (firstResult.value) { + if (correctionOutput !== "") { + const h3 = document.createElement("h3"); + h3.innerHTML = correctionOutput; + output.appendChild(h3); + } + await addNextResultToOutput(firstResult.value); + } else { + output = document.createElement("div"); + if (correctionOutput !== "") { + const h3 = document.createElement("h3"); + h3.innerHTML = correctionOutput; + output.appendChild(h3); + } output.className = "search-failed" + extraClass; - output.innerHTML = "No results :(<br/>" + + const dlroChannel = `https://doc.rust-lang.org/${getVar("channel")}`; + if (query.userQuery !== "") { + output.innerHTML += "No results :(<br/>" + "Try on <a href=\"https://duckduckgo.com/?q=" + encodeURIComponent("rust " + query.userQuery) + "\">DuckDuckGo</a>?<br/><br/>" + @@ -4922,192 +4668,194 @@ ${item.displayPath}<span class="${type}">${name}</span>\ "introductions to language features and the language itself.</li><li><a " + "href=\"https://docs.rs\">Docs.rs</a> for documentation of crates released on" + " <a href=\"https://crates.io/\">crates.io</a>.</li></ul>"; + } + output.innerHTML += "Example searches:<ul>" + + "<li><a href=\"" + getNakedUrl() + "?search=std::vec\">std::vec</a></li>" + + "<li><a href=\"" + getNakedUrl() + "?search=u32+->+bool\">u32 -> bool</a></li>" + + "<li><a href=\"" + getNakedUrl() + "?search=Option<T>,+(T+->+U)+->+Option<U>\">" + + "Option<T>, (T -> U) -> Option<U></a></li>" + + "</ul>"; + // need to make sure the element is shown before + // running this callback + yieldToBrowser().then(() => finishedCallback(0, output)); } return output; } -// @ts-expect-error -function makeTabHeader(tabNb, text, nbElems) { - // https://blog.horizon-eda.org/misc/2020/02/19/ui.html - // - // CSS runs with `font-variant-numeric: tabular-nums` to ensure all - // digits are the same width. \u{2007} is a Unicode space character - // that is defined to be the same width as a digit. - const fmtNbElems = - nbElems < 10 ? `\u{2007}(${nbElems})\u{2007}\u{2007}` : - nbElems < 100 ? `\u{2007}(${nbElems})\u{2007}` : `\u{2007}(${nbElems})`; - if (searchState.currentTab === tabNb) { - return "<button class=\"selected\">" + text + - "<span class=\"count\">" + fmtNbElems + "</span></button>"; - } - return "<button>" + text + "<span class=\"count\">" + fmtNbElems + "</span></button>"; +/** + * returns [tab, output] + * @param {number} tabNb + * @param {string} text + * @param {AsyncGenerator<rustdoc.ResultObject>} results + * @param {rustdoc.ParsedQuery<rustdoc.ParserQueryElement>} query + * @param {boolean} isTypeSearch + * @param {boolean} goToFirst + * @returns {[HTMLElement, Promise<HTMLElement>]} + */ +function makeTab(tabNb, text, results, query, isTypeSearch, goToFirst) { + const isCurrentTab = window.searchState.currentTab === tabNb; + const tabButton = document.createElement("button"); + tabButton.appendChild(document.createTextNode(text)); + tabButton.className = isCurrentTab ? "selected" : ""; + const tabCount = document.createElement("span"); + tabCount.className = "count loading"; + tabCount.innerHTML = "\u{2007}(\u{2007})\u{2007}\u{2007}"; + tabButton.appendChild(tabCount); + return [ + tabButton, + addTab(results, query, isCurrentTab, (count, output) => { + const search = window.searchState.outputElement(); + const error = query.error; + if (count === 0 && error !== null && search) { + error.forEach((value, index) => { + value = value.split("<").join("<").split(">").join(">"); + if (index % 2 !== 0) { + error[index] = `<code>${value.replaceAll(" ", " ")}</code>`; + } else { + error[index] = value; + } + }); + const errorReport = document.createElement("h3"); + errorReport.className = "error"; + errorReport.innerHTML = `Query parser error: "${error.join("")}".`; + search.insertBefore(errorReport, search.firstElementChild); + } else if (goToFirst || + (count === 1 && getSettingValue("go-to-only-result") === "true") + ) { + // Needed to force re-execution of JS when coming back to a page. Let's take this + // scenario as example: + // + // 1. You have the "Directly go to item in search if there is only one result" + // option enabled. + // 2. You make a search which results only one result, leading you automatically to + // this result. + // 3. You go back to previous page. + // + // Now, without the call below, the JS will not be re-executed and the previous + // state will be used, starting search again since the search input is not empty, + // leading you back to the previous page again. + window.onunload = () => { }; + window.searchState.removeQueryParameters(); + const a = output.querySelector("a"); + if (a) { + a.click(); + return; + } + } + + // https://blog.horizon-eda.org/misc/2020/02/19/ui.html + // + // CSS runs with `font-variant-numeric: tabular-nums` to ensure all + // digits are the same width. \u{2007} is a Unicode space character + // that is defined to be the same width as a digit. + const fmtNbElems = + count < 10 ? `\u{2007}(${count})\u{2007}\u{2007}` : + count < 100 ? `\u{2007}(${count})\u{2007}` : `\u{2007}(${count})`; + tabCount.innerHTML = fmtNbElems; + tabCount.className = "count"; + }, isTypeSearch), + ]; } /** + * @param {DocSearch} docSearch * @param {rustdoc.ResultsTable} results - * @param {boolean} go_to_first + * @param {boolean} goToFirst * @param {string} filterCrates */ -async function showResults(results, go_to_first, filterCrates) { - const search = searchState.outputElement(); - if (go_to_first || (results.others.length === 1 - && getSettingValue("go-to-only-result") === "true") - ) { - // Needed to force re-execution of JS when coming back to a page. Let's take this - // scenario as example: - // - // 1. You have the "Directly go to item in search if there is only one result" option - // enabled. - // 2. You make a search which results only one result, leading you automatically to - // this result. - // 3. You go back to previous page. - // - // Now, without the call below, the JS will not be re-executed and the previous state - // will be used, starting search again since the search input is not empty, leading you - // back to the previous page again. - window.onunload = () => { }; - searchState.removeQueryParameters(); - const elem = document.createElement("a"); - elem.href = results.others[0].href; - removeClass(elem, "active"); - // For firefox, we need the element to be in the DOM so it can be clicked. - document.body.appendChild(elem); - elem.click(); - return; - } - if (results.query === undefined) { - // @ts-expect-error - results.query = DocSearch.parseQuery(searchState.input.value); - } +async function showResults(docSearch, results, goToFirst, filterCrates) { + const search = window.searchState.outputElement(); - currentResults = results.query.userQuery; - - // 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 && 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 (results.in_args.length) { - currentTab = 1; - } else if (results.returned.length) { - currentTab = 2; - } + if (!search) { + return; } let crates = ""; - if (rawSearchIndex.size > 1) { - crates = "<div class=\"sub-heading\"> in <div id=\"crate-search-div\">" + + const crateNames = await docSearch.getCrateNameList(); + if (crateNames.length > 1) { + crates = " in <div id=\"crate-search-div\">" + "<select id=\"crate-search\"><option value=\"all crates\">all crates</option>"; - for (const c of rawSearchIndex.keys()) { + const l = crateNames.length; + for (let i = 0; i < l; i += 1) { + const c = crateNames[i]; crates += `<option value="${c}" ${c === filterCrates && "selected"}>${c}</option>`; } - crates += "</select></div></div>"; + crates += "</select></div>"; } + nonnull(document.querySelector(".search-switcher")).innerHTML = `Search results${crates}`; - let output = `<div class="main-heading">\ - <h1 class="search-results-title">Results</h1>${crates}</div>`; + /** @type {[HTMLElement, Promise<HTMLElement>][]} */ + const tabs = []; + searchState.currentTab = 0; if (results.query.error !== null) { - const error = results.query.error; - // @ts-expect-error - error.forEach((value, index) => { - value = value.split("<").join("<").split(">").join(">"); - if (index % 2 !== 0) { - error[index] = `<code>${value.replaceAll(" ", " ")}</code>`; - } else { - error[index] = value; - } - }); - output += `<h3 class="error">Query parser error: "${error.join("")}".</h3>`; - output += "<div id=\"search-tabs\">" + - makeTabHeader(0, "In Names", results.others.length) + - "</div>"; - currentTab = 0; + tabs.push(makeTab(0, "In Names", results.others, results.query, false, goToFirst)); } else if (results.query.foundElems <= 1 && results.query.returned.length === 0) { - output += "<div id=\"search-tabs\">" + - makeTabHeader(0, "In Names", results.others.length) + - makeTabHeader(1, "In Parameters", results.in_args.length) + - makeTabHeader(2, "In Return Types", results.returned.length) + - "</div>"; + tabs.push(makeTab(0, "In Names", results.others, results.query, false, goToFirst)); + tabs.push(makeTab(1, "In Parameters", results.in_args, results.query, true, false)); + tabs.push(makeTab(2, "In Return Types", results.returned, results.query, true, false)); } else { const signatureTabTitle = results.query.elems.length === 0 ? "In Function Return Types" : results.query.returned.length === 0 ? "In Function Parameters" : "In Function Signatures"; - output += "<div id=\"search-tabs\">" + - makeTabHeader(0, signatureTabTitle, results.others.length) + - "</div>"; - currentTab = 0; - } - - if (results.query.correction !== null) { - const orig = results.query.returned.length > 0 - ? results.query.returned[0].name - : results.query.elems[0].name; - output += "<h3 class=\"search-corrections\">" + - `Type "${orig}" not found. ` + - "Showing results for closest type name " + - `"${results.query.correction}" instead.</h3>`; - } - if (results.query.proposeCorrectionFrom !== null) { - const orig = results.query.proposeCorrectionFrom; - const targ = results.query.proposeCorrectionTo; - output += "<h3 class=\"search-corrections\">" + - `Type "${orig}" not found and used as generic parameter. ` + - `Consider searching for "${targ}" instead.</h3>`; + tabs.push(makeTab(0, signatureTabTitle, results.others, results.query, true, goToFirst)); } - 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 tabsElem = document.createElement("div"); + tabsElem.id = "search-tabs"; const resultsElem = document.createElement("div"); resultsElem.id = "results"; - resultsElem.appendChild(ret_others); - resultsElem.appendChild(ret_in_args); - resultsElem.appendChild(ret_returned); - // @ts-expect-error - search.innerHTML = output; - if (searchState.rustdocToolbar) { - // @ts-expect-error - search.querySelector(".main-heading").appendChild(searchState.rustdocToolbar); + search.innerHTML = ""; + for (const [tab, output] of tabs) { + tabsElem.appendChild(tab); + const placeholder = document.createElement("div"); + output.then(output => { + if (placeholder.parentElement) { + placeholder.parentElement.replaceChild(output, placeholder); + } + }); + resultsElem.appendChild(placeholder); + } + + if (window.searchState.rustdocToolbar) { + nonnull( + nonnull(window.searchState.containerElement()) + .querySelector(".main-heading"), + ).appendChild(window.searchState.rustdocToolbar); } const crateSearch = document.getElementById("crate-search"); if (crateSearch) { crateSearch.addEventListener("input", updateCrate); } - // @ts-expect-error + search.appendChild(tabsElem); search.appendChild(resultsElem); // Reset focused elements. - searchState.showResults(search); - // @ts-expect-error - const elems = document.getElementById("search-tabs").childNodes; - // @ts-expect-error - searchState.focusedByTab = []; + window.searchState.showResults(); + window.searchState.focusedByTab = [null, null, null]; let i = 0; - for (const elem of elems) { + for (const elem of tabsElem.childNodes) { const j = i; // @ts-expect-error elem.onclick = () => printTab(j); - searchState.focusedByTab.push(null); + window.searchState.focusedByTab[i] = null; i += 1; } - printTab(currentTab); + printTab(0); } // @ts-expect-error function updateSearchHistory(url) { + const btn = document.querySelector("#search-button a"); + if (btn instanceof HTMLAnchorElement) { + btn.href = url; + } if (!browserSupportsHistoryApi()) { return; } const params = searchState.getQueryStringParams(); - if (!history.state && !params.search) { + if (!history.state && params.search === undefined) { history.pushState(null, "", url); } else { history.replaceState(null, "", url); @@ -5120,8 +4868,8 @@ function updateSearchHistory(url) { * @param {boolean} [forced] */ async function search(forced) { - // @ts-expect-error - const query = DocSearch.parseQuery(searchState.input.value.trim()); + const query = DocSearch.parseQuery(nonnull(window.searchState.inputElement()).value.trim()); + let filterCrates = getFilterCrates(); // @ts-expect-error @@ -5131,6 +4879,7 @@ async function search(forced) { } return; } + currentResults = query.userQuery; searchState.setLoadingSearch(); @@ -5142,6 +4891,12 @@ async function search(forced) { filterCrates = params["filter-crate"]; } + if (filterCrates !== null && + (await docSearch.getCrateNameList()).indexOf(filterCrates) === -1 + ) { + filterCrates = null; + } + // Update document title to maintain a meaningful browser history searchState.title = "\"" + query.userQuery + "\" Search - Rust"; @@ -5150,6 +4905,7 @@ async function search(forced) { updateSearchHistory(buildUrl(query.userQuery, filterCrates)); await showResults( + docSearch, // @ts-expect-error await docSearch.execQuery(query, filterCrates, window.currentCrate), params.go_to_first, @@ -5169,16 +4925,14 @@ function onSearchSubmit(e) { } function putBackSearch() { - const search_input = searchState.input; - if (!searchState.input) { + const search_input = window.searchState.inputElement(); + if (!search_input) { return; } - // @ts-expect-error if (search_input.value !== "" && !searchState.isDisplayed()) { searchState.showResults(); if (browserSupportsHistoryApi()) { history.replaceState(null, "", - // @ts-expect-error buildUrl(search_input.value, getFilterCrates())); } document.title = searchState.title; @@ -5192,30 +4946,21 @@ function registerSearchEvents() { // but only if the input bar is empty. This avoid the obnoxious issue // where you start trying to do a search, and the index loads, and // suddenly your search is gone! - // @ts-expect-error - if (searchState.input.value === "") { - // @ts-expect-error - searchState.input.value = params.search || ""; + const inputElement = nonnull(window.searchState.inputElement()); + if (inputElement.value === "") { + inputElement.value = params.search || ""; } const searchAfter500ms = () => { searchState.clearInputTimeout(); - // @ts-expect-error - if (searchState.input.value.length === 0) { - searchState.hideResults(); - } else { - // @ts-ignore - searchState.timeout = setTimeout(search, 500); - } + window.searchState.timeout = setTimeout(search, 500); }; - // @ts-expect-error - searchState.input.onkeyup = searchAfter500ms; - // @ts-expect-error - searchState.input.oninput = searchAfter500ms; - // @ts-expect-error - document.getElementsByClassName("search-form")[0].onsubmit = onSearchSubmit; - // @ts-expect-error - searchState.input.onchange = e => { + inputElement.onkeyup = searchAfter500ms; + inputElement.oninput = searchAfter500ms; + if (inputElement.form) { + inputElement.form.onsubmit = onSearchSubmit; + } + inputElement.onchange = e => { if (e.target !== document.activeElement) { // To prevent doing anything when it's from a blur event. return; @@ -5227,11 +4972,13 @@ function registerSearchEvents() { // change, though. setTimeout(search, 0); }; - // @ts-expect-error - searchState.input.onpaste = searchState.input.onchange; + inputElement.onpaste = inputElement.onchange; // @ts-expect-error searchState.outputElement().addEventListener("keydown", e => { + if (!(e instanceof KeyboardEvent)) { + return; + } // We only handle unmodified keystrokes here. We don't want to interfere with, // for instance, alt-left and alt-right for history navigation. if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) { @@ -5271,88 +5018,23 @@ function registerSearchEvents() { } }); - // @ts-expect-error - searchState.input.addEventListener("keydown", e => { + inputElement.addEventListener("keydown", e => { if (e.which === 40) { // down focusSearchResult(); e.preventDefault(); } }); - // @ts-expect-error - searchState.input.addEventListener("focus", () => { + inputElement.addEventListener("focus", () => { putBackSearch(); }); - - // @ts-expect-error - searchState.input.addEventListener("blur", () => { - if (window.searchState.input) { - window.searchState.input.placeholder = window.searchState.origPlaceholder; - } - }); - - // Push and pop states are used to add search results to the browser - // history. - if (browserSupportsHistoryApi()) { - // Store the previous <title> so we can revert back to it later. - const previousTitle = document.title; - - window.addEventListener("popstate", e => { - const params = searchState.getQueryStringParams(); - // Revert to the previous title manually since the History - // API ignores the title parameter. - document.title = previousTitle; - // When browsing forward to search results the previous - // search will be repeated, so the currentResults are - // cleared to ensure the search is successful. - currentResults = null; - // Synchronize search bar with query string state and - // perform the search. This will empty the bar if there's - // nothing there, which lets you really go back to a - // previous state with nothing in the bar. - if (params.search && params.search.length > 0) { - // @ts-expect-error - searchState.input.value = params.search; - // Some browsers fire "onpopstate" for every page load - // (Chrome), while others fire the event only when actually - // popping a state (Firefox), which is why search() is - // called both here and at the end of the startSearch() - // function. - e.preventDefault(); - search(); - } else { - // @ts-expect-error - searchState.input.value = ""; - // When browsing back from search results the main page - // visibility must be reset. - searchState.hideResults(); - } - }); - } - - // This is required in firefox to avoid this problem: Navigating to a search result - // with the keyboard, hitting enter, and then hitting back would take you back to - // the doc page, rather than the search that should overlay it. - // This was an interaction between the back-forward cache and our handlers - // that try to sync state between the URL and the search input. To work around it, - // do a small amount of re-init on page show. - window.onpageshow = () => { - const qSearch = searchState.getQueryStringParams().search; - // @ts-expect-error - if (searchState.input.value === "" && qSearch) { - // @ts-expect-error - searchState.input.value = qSearch; - } - search(); - }; } // @ts-expect-error function updateCrate(ev) { if (ev.target.value === "all crates") { // If we don't remove it from the URL, it'll be picked up again by the search. - // @ts-expect-error - const query = searchState.input.value.trim(); + const query = nonnull(window.searchState.inputElement()).value.trim(); updateSearchHistory(buildUrl(query, null)); } // In case you "cut" the entry from the search input, then change the crate filter @@ -5362,522 +5044,86 @@ function updateCrate(ev) { search(true); } -// Parts of this code are based on Lucene, which is licensed under the -// Apache/2.0 license. -// More information found here: -// https://fossies.org/linux/lucene/lucene/core/src/java/org/apache/lucene/util/automaton/ -// LevenshteinAutomata.java -class ParametricDescription { - // @ts-expect-error - constructor(w, n, minErrors) { - this.w = w; - this.n = n; - this.minErrors = minErrors; - } - // @ts-expect-error - isAccept(absState) { - const state = Math.floor(absState / (this.w + 1)); - const offset = absState % (this.w + 1); - return this.w - offset + this.minErrors[state] <= this.n; - } - // @ts-expect-error - getPosition(absState) { - return absState % (this.w + 1); - } - // @ts-expect-error - getVector(name, charCode, pos, end) { - let vector = 0; - for (let i = pos; i < end; i += 1) { - vector = vector << 1; - if (name.charCodeAt(i) === charCode) { - vector |= 1; - } - } - return vector; - } - // @ts-expect-error - unpack(data, index, bitsPerValue) { - const bitLoc = (bitsPerValue * index); - const dataLoc = bitLoc >> 5; - const bitStart = bitLoc & 31; - if (bitStart + bitsPerValue <= 32) { - // not split - return ((data[dataLoc] >> bitStart) & this.MASKS[bitsPerValue - 1]); - } else { - // split - const part = 32 - bitStart; - return ~~(((data[dataLoc] >> bitStart) & this.MASKS[part - 1]) + - ((data[1 + dataLoc] & this.MASKS[bitsPerValue - part - 1]) << part)); - } +// eslint-disable-next-line max-len +// polyfill https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array/fromBase64 +/** + * @type {function(string): Uint8Array} base64 + */ +//@ts-expect-error +const makeUint8ArrayFromBase64 = Uint8Array.fromBase64 ? Uint8Array.fromBase64 : (string => { + const bytes_as_string = atob(string); + const l = bytes_as_string.length; + const bytes = new Uint8Array(l); + for (let i = 0; i < l; ++i) { + bytes[i] = bytes_as_string.charCodeAt(i); } -} -ParametricDescription.prototype.MASKS = new Int32Array([ - 0x1, 0x3, 0x7, 0xF, - 0x1F, 0x3F, 0x7F, 0xFF, - 0x1FF, 0x3F, 0x7FF, 0xFFF, - 0x1FFF, 0x3FFF, 0x7FFF, 0xFFFF, - 0x1FFFF, 0x3FFFF, 0x7FFFF, 0xFFFFF, - 0x1FFFFF, 0x3FFFFF, 0x7FFFFF, 0xFFFFFF, - 0x1FFFFFF, 0x3FFFFFF, 0x7FFFFFF, 0xFFFFFFF, - 0x1FFFFFFF, 0x3FFFFFFF, 0x7FFFFFFF, 0xFFFFFFFF, -]); - -// The following code was generated with the moman/finenight pkg -// This package is available under the MIT License, see NOTICE.txt -// for more details. -// This class is auto-generated, Please do not modify it directly. -// You should modify the https://gitlab.com/notriddle/createAutomata.py instead. -// The following code was generated with the moman/finenight pkg -// This package is available under the MIT License, see NOTICE.txt -// for more details. -// This class is auto-generated, Please do not modify it directly. -// You should modify https://gitlab.com/notriddle/moman-rustdoc instead. - -class Lev2TParametricDescription extends ParametricDescription { - /** - * @param {number} absState - * @param {number} position - * @param {number} vector - * @returns {number} - */ - transition(absState, position, vector) { - let state = Math.floor(absState / (this.w + 1)); - let offset = absState % (this.w + 1); - - if (position === this.w) { - if (state < 3) { // eslint-disable-line no-lonely-if - const loc = Math.imul(vector, 3) + state; - offset += this.unpack(this.offsetIncrs0, loc, 1); - state = this.unpack(this.toStates0, loc, 2) - 1; - } - } else if (position === this.w - 1) { - if (state < 5) { // eslint-disable-line no-lonely-if - const loc = Math.imul(vector, 5) + state; - offset += this.unpack(this.offsetIncrs1, loc, 1); - state = this.unpack(this.toStates1, loc, 3) - 1; - } - } else if (position === this.w - 2) { - if (state < 13) { // eslint-disable-line no-lonely-if - const loc = Math.imul(vector, 13) + state; - offset += this.unpack(this.offsetIncrs2, loc, 2); - state = this.unpack(this.toStates2, loc, 4) - 1; - } - } else if (position === this.w - 3) { - if (state < 28) { // eslint-disable-line no-lonely-if - const loc = Math.imul(vector, 28) + state; - offset += this.unpack(this.offsetIncrs3, loc, 2); - state = this.unpack(this.toStates3, loc, 5) - 1; - } - } else if (position === this.w - 4) { - if (state < 45) { // eslint-disable-line no-lonely-if - const loc = Math.imul(vector, 45) + state; - offset += this.unpack(this.offsetIncrs4, loc, 3); - state = this.unpack(this.toStates4, loc, 6) - 1; - } - } else { - if (state < 45) { // eslint-disable-line no-lonely-if - const loc = Math.imul(vector, 45) + state; - offset += this.unpack(this.offsetIncrs5, loc, 3); - state = this.unpack(this.toStates5, loc, 6) - 1; - } - } + return bytes; +}); - if (state === -1) { - // null state - return -1; - } else { - // translate back to abs - return Math.imul(state, this.w + 1) + offset; - } - } - // state map - // 0 -> [(0, 0)] - // 1 -> [(0, 1)] - // 2 -> [(0, 2)] - // 3 -> [(0, 1), (1, 1)] - // 4 -> [(0, 2), (1, 2)] - // 5 -> [(0, 1), (1, 1), (2, 1)] - // 6 -> [(0, 2), (1, 2), (2, 2)] - // 7 -> [(0, 1), (2, 1)] - // 8 -> [(0, 1), (2, 2)] - // 9 -> [(0, 2), (2, 1)] - // 10 -> [(0, 2), (2, 2)] - // 11 -> [t(0, 1), (0, 1), (1, 1), (2, 1)] - // 12 -> [t(0, 2), (0, 2), (1, 2), (2, 2)] - // 13 -> [(0, 2), (1, 2), (2, 2), (3, 2)] - // 14 -> [(0, 1), (1, 1), (3, 2)] - // 15 -> [(0, 1), (2, 2), (3, 2)] - // 16 -> [(0, 1), (3, 2)] - // 17 -> [(0, 1), t(1, 2), (2, 2), (3, 2)] - // 18 -> [(0, 2), (1, 2), (3, 1)] - // 19 -> [(0, 2), (1, 2), (3, 2)] - // 20 -> [(0, 2), (1, 2), t(1, 2), (2, 2), (3, 2)] - // 21 -> [(0, 2), (2, 1), (3, 1)] - // 22 -> [(0, 2), (2, 2), (3, 2)] - // 23 -> [(0, 2), (3, 1)] - // 24 -> [(0, 2), (3, 2)] - // 25 -> [(0, 2), t(1, 2), (1, 2), (2, 2), (3, 2)] - // 26 -> [t(0, 2), (0, 2), (1, 2), (2, 2), (3, 2)] - // 27 -> [t(0, 2), (0, 2), (1, 2), (3, 1)] - // 28 -> [(0, 2), (1, 2), (2, 2), (3, 2), (4, 2)] - // 29 -> [(0, 2), (1, 2), (2, 2), (4, 2)] - // 30 -> [(0, 2), (1, 2), (2, 2), t(2, 2), (3, 2), (4, 2)] - // 31 -> [(0, 2), (1, 2), (3, 2), (4, 2)] - // 32 -> [(0, 2), (1, 2), (4, 2)] - // 33 -> [(0, 2), (1, 2), t(1, 2), (2, 2), (3, 2), (4, 2)] - // 34 -> [(0, 2), (1, 2), t(2, 2), (2, 2), (3, 2), (4, 2)] - // 35 -> [(0, 2), (2, 1), (4, 2)] - // 36 -> [(0, 2), (2, 2), (3, 2), (4, 2)] - // 37 -> [(0, 2), (2, 2), (4, 2)] - // 38 -> [(0, 2), (3, 2), (4, 2)] - // 39 -> [(0, 2), (4, 2)] - // 40 -> [(0, 2), t(1, 2), (1, 2), (2, 2), (3, 2), (4, 2)] - // 41 -> [(0, 2), t(2, 2), (2, 2), (3, 2), (4, 2)] - // 42 -> [t(0, 2), (0, 2), (1, 2), (2, 2), (3, 2), (4, 2)] - // 43 -> [t(0, 2), (0, 2), (1, 2), (2, 2), (4, 2)] - // 44 -> [t(0, 2), (0, 2), (1, 2), (2, 2), t(2, 2), (3, 2), (4, 2)] - - - /** @param {number} w - length of word being checked */ - constructor(w) { - super(w, 2, new Int32Array([ - 0,1,2,0,1,-1,0,-1,0,-1,0,-1,0,-1,-1,-1,-1,-1,-2,-1,-1,-2,-1,-2, - -1,-1,-1,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2, - ])); +if (ROOT_PATH === null) { + return; +} +const database = await Stringdex.loadDatabase(hooks); +if (typeof window !== "undefined") { + docSearch = new DocSearch(ROOT_PATH, database); + await docSearch.buildIndex(); + registerSearchEvents(); + // If there's a search term in the URL, execute the search now. + if (window.searchState.getQueryStringParams().search !== undefined) { + search(); } +} else if (typeof exports !== "undefined") { + docSearch = new DocSearch(ROOT_PATH, database); + await docSearch.buildIndex(); + return { docSearch, DocSearch }; } +}; -Lev2TParametricDescription.prototype.toStates0 = /*2 bits per value */ new Int32Array([ - 0xe, -]); -Lev2TParametricDescription.prototype.offsetIncrs0 = /*1 bits per value */ new Int32Array([ - 0x0, -]); - -Lev2TParametricDescription.prototype.toStates1 = /*3 bits per value */ new Int32Array([ - 0x1a688a2c, -]); -Lev2TParametricDescription.prototype.offsetIncrs1 = /*1 bits per value */ new Int32Array([ - 0x3e0, -]); - -Lev2TParametricDescription.prototype.toStates2 = /*4 bits per value */ new Int32Array([ - 0x70707054,0xdc07035,0x3dd3a3a,0x2323213a, - 0x15435223,0x22545432,0x5435, -]); -Lev2TParametricDescription.prototype.offsetIncrs2 = /*2 bits per value */ new Int32Array([ - 0x80000,0x55582088,0x55555555,0x55, -]); - -Lev2TParametricDescription.prototype.toStates3 = /*5 bits per value */ new Int32Array([ - 0x1c0380a4,0x700a570,0xca529c0,0x180a00, - 0xa80af180,0xc5498e60,0x5a546398,0x8c4300e8, - 0xac18c601,0xd8d43501,0x863500ad,0x51976d6a, - 0x8ca0180a,0xc3501ac2,0xb0c5be16,0x76dda8a5, - 0x18c4519,0xc41294a,0xe248d231,0x1086520c, - 0xce31ac42,0x13946358,0x2d0348c4,0x6732d494, - 0x1ad224a5,0xd635ad4b,0x520c4139,0xce24948, - 0x22110a52,0x58ce729d,0xc41394e3,0x941cc520, - 0x90e732d4,0x4729d224,0x39ce35ad, -]); -Lev2TParametricDescription.prototype.offsetIncrs3 = /*2 bits per value */ new Int32Array([ - 0x80000,0xc0c830,0x300f3c30,0x2200fcff, - 0xcaa00a08,0x3c2200a8,0xa8fea00a,0x55555555, - 0x55555555,0x55555555,0x55555555,0x55555555, - 0x55555555,0x55555555, -]); - -Lev2TParametricDescription.prototype.toStates4 = /*6 bits per value */ new Int32Array([ - 0x801c0144,0x1453803,0x14700038,0xc0005145, - 0x1401,0x14,0x140000,0x0, - 0x510000,0x6301f007,0x301f00d1,0xa186178, - 0xc20ca0c3,0xc20c30,0xc30030c,0xc00c00cd, - 0xf0c00c30,0x4c054014,0xc30944c3,0x55150c34, - 0x8300550,0x430c0143,0x50c31,0xc30850c, - 0xc3143000,0x50053c50,0x5130d301,0x850d30c2, - 0x30a08608,0xc214414,0x43142145,0x21450031, - 0x1400c314,0x4c143145,0x32832803,0x28014d6c, - 0xcd34a0c3,0x1c50c76,0x1c314014,0x430c30c3, - 0x1431,0xc300500,0xca00d303,0xd36d0e40, - 0x90b0e400,0xcb2abb2c,0x70c20ca1,0x2c32ca2c, - 0xcd2c70cb,0x31c00c00,0x34c2c32c,0x5583280, - 0x558309b7,0x6cd6ca14,0x430850c7,0x51c51401, - 0x1430c714,0xc3087,0x71451450,0xca00d30, - 0xc26dc156,0xb9071560,0x1cb2abb2,0xc70c2144, - 0xb1c51ca1,0x1421c70c,0xc51c00c3,0x30811c51, - 0x24324308,0xc51031c2,0x70820820,0x5c33830d, - 0xc33850c3,0x30c30c30,0xc30c31c,0x451450c3, - 0x20c20c20,0xda0920d,0x5145914f,0x36596114, - 0x51965865,0xd9643653,0x365a6590,0x51964364, - 0x43081505,0x920b2032,0x2c718b28,0xd7242249, - 0x35cb28b0,0x2cb3872c,0x972c30d7,0xb0c32cb2, - 0x4e1c75c,0xc80c90c2,0x62ca2482,0x4504171c, - 0xd65d9610,0x33976585,0xd95cb5d,0x4b5ca5d7, - 0x73975c36,0x10308138,0xc2245105,0x41451031, - 0x14e24208,0xc35c3387,0x51453851,0x1c51c514, - 0xc70c30c3,0x20451450,0x14f1440c,0x4f0da092, - 0x4513d41,0x6533944d,0x1350e658,0xe1545055, - 0x64365a50,0x5519383,0x51030815,0x28920718, - 0x441c718b,0x714e2422,0x1c35cb28,0x4e1c7387, - 0xb28e1c51,0x5c70c32c,0xc204e1c7,0x81c61440, - 0x1c62ca24,0xd04503ce,0x85d63944,0x39338e65, - 0x8e154387,0x364b5ca3,0x38739738, -]); -Lev2TParametricDescription.prototype.offsetIncrs4 = /*3 bits per value */ new Int32Array([ - 0x10000000,0xc00000,0x60061,0x400, - 0x0,0x80010008,0x249248a4,0x8229048, - 0x2092,0x6c3603,0xb61b6c30,0x6db6036d, - 0xdb6c0,0x361b0180,0x91b72000,0xdb11b71b, - 0x6db6236,0x1008200,0x12480012,0x24924906, - 0x48200049,0x80410002,0x24000900,0x4924a489, - 0x10822492,0x20800125,0x48360,0x9241b692, - 0x6da4924,0x40009268,0x241b010,0x291b4900, - 0x6d249249,0x49493423,0x92492492,0x24924924, - 0x49249249,0x92492492,0x24924924,0x49249249, - 0x92492492,0x24924924,0x49249249,0x92492492, - 0x24924924,0x49249249,0x92492492,0x24924924, - 0x49249249,0x92492492,0x24924924,0x49249249, - 0x92492492,0x24924924,0x49249249,0x92492492, - 0x24924924,0x49249249,0x92492492,0x24924924, - 0x49249249,0x92492492,0x24924924,0x49249249, - 0x92492492,0x24924924,0x49249249,0x2492, -]); - -Lev2TParametricDescription.prototype.toStates5 = /*6 bits per value */ new Int32Array([ - 0x801c0144,0x1453803,0x14700038,0xc0005145, - 0x1401,0x14,0x140000,0x0, - 0x510000,0x4e00e007,0xe0051,0x3451451c, - 0xd015000,0x30cd0000,0xc30c30c,0xc30c30d4, - 0x40c30c30,0x7c01c014,0xc03458c0,0x185e0c07, - 0x2830c286,0x830c3083,0xc30030,0x33430c, - 0x30c3003,0x70051030,0x16301f00,0x8301f00d, - 0x30a18617,0xc20ca0c,0x431420c3,0xb1450c51, - 0x14314315,0x4f143145,0x34c05401,0x4c30944c, - 0x55150c3,0x30830055,0x1430c014,0xc00050c3, - 0xc30850,0xc314300,0x150053c5,0x25130d30, - 0x5430d30c,0xc0354154,0x300d0c90,0x1cb2cd0c, - 0xc91cb0c3,0x72c30cb2,0x14f1cb2c,0xc34c0540, - 0x34c30944,0x82182214,0x851050c2,0x50851430, - 0x1400c50c,0x30c5085,0x50c51450,0x150053c, - 0xc25130d3,0x8850d30,0x1430a086,0x450c2144, - 0x51cb1c21,0x1c91c70c,0xc71c314b,0x34c1cb1, - 0x6c328328,0xc328014d,0x76cd34a0,0x1401c50c, - 0xc31c3140,0x31430c30,0x14,0x30c3005, - 0xa0ca00d3,0x535b0c,0x4d2830ca,0x514369b3, - 0xc500d01,0x5965965a,0x30d46546,0x6435030c, - 0x8034c659,0xdb439032,0x2c390034,0xcaaecb24, - 0x30832872,0xcb28b1c,0x4b1c32cb,0x70030033, - 0x30b0cb0c,0xe40ca00d,0x400d36d0,0xb2c90b0e, - 0xca1cb2ab,0xa2c70c20,0x6575d95c,0x4315b5ce, - 0x95c53831,0x28034c5d,0x9b705583,0xa1455830, - 0xc76cd6c,0x40143085,0x71451c51,0x871430c, - 0x450000c3,0xd3071451,0x1560ca00,0x560c26dc, - 0xb35b2851,0xc914369,0x1a14500d,0x46593945, - 0xcb2c939,0x94507503,0x328034c3,0x9b70558, - 0xe41c5583,0x72caaeca,0x1c308510,0xc7147287, - 0x50871c32,0x1470030c,0xd307147,0xc1560ca0, - 0x1560c26d,0xabb2b907,0x21441cb2,0x38a1c70c, - 0x8e657394,0x314b1c93,0x39438738,0x43083081, - 0x31c22432,0x820c510,0x830d7082,0x50c35c33, - 0xc30c338,0xc31c30c3,0x50c30c30,0xc204514, - 0x890c90c2,0x31440c70,0xa8208208,0xea0df0c3, - 0x8a231430,0xa28a28a2,0x28a28a1e,0x1861868a, - 0x48308308,0xc3682483,0x14516453,0x4d965845, - 0xd4659619,0x36590d94,0xd969964,0x546590d9, - 0x20c20541,0x920d20c,0x5914f0da,0x96114514, - 0x65865365,0xe89d3519,0x99e7a279,0x9e89e89e, - 0x81821827,0xb2032430,0x18b28920,0x422492c7, - 0xb28b0d72,0x3872c35c,0xc30d72cb,0x32cb2972, - 0x1c75cb0c,0xc90c204e,0xa2482c80,0x24b1c62c, - 0xc3a89089,0xb0ea2e42,0x9669a31c,0xa4966a28, - 0x59a8a269,0x8175e7a,0xb203243,0x718b2892, - 0x4114105c,0x17597658,0x74ce5d96,0x5c36572d, - 0xd92d7297,0xe1ce5d70,0xc90c204,0xca2482c8, - 0x4171c62,0x5d961045,0x976585d6,0x79669533, - 0x964965a2,0x659689e6,0x308175e7,0x24510510, - 0x451031c2,0xe2420841,0x5c338714,0x453851c3, - 0x51c51451,0xc30c31c,0x451450c7,0x41440c20, - 0xc708914,0x82105144,0xf1c58c90,0x1470ea0d, - 0x61861863,0x8a1e85e8,0x8687a8a2,0x3081861, - 0x24853c51,0x5053c368,0x1341144f,0x96194ce5, - 0x1544d439,0x94385514,0xe0d90d96,0x5415464, - 0x4f1440c2,0xf0da0921,0x4513d414,0x533944d0, - 0x350e6586,0x86082181,0xe89e981d,0x18277689, - 0x10308182,0x89207185,0x41c718b2,0x14e24224, - 0xc35cb287,0xe1c73871,0x28e1c514,0xc70c32cb, - 0x204e1c75,0x1c61440c,0xc62ca248,0x90891071, - 0x2e41c58c,0xa31c70ea,0xe86175e7,0xa269a475, - 0x5e7a57a8,0x51030817,0x28920718,0xf38718b, - 0xe5134114,0x39961758,0xe1ce4ce,0x728e3855, - 0x5ce0d92d,0xc204e1ce,0x81c61440,0x1c62ca24, - 0xd04503ce,0x85d63944,0x75338e65,0x5d86075e, - 0x89e69647,0x75e76576, -]); -Lev2TParametricDescription.prototype.offsetIncrs5 = /*3 bits per value */ new Int32Array([ - 0x10000000,0xc00000,0x60061,0x400, - 0x0,0x60000008,0x6b003080,0xdb6ab6db, - 0x2db6,0x800400,0x49245240,0x11482412, - 0x104904,0x40020000,0x92292000,0xa4b25924, - 0x9649658,0xd80c000,0xdb0c001b,0x80db6d86, - 0x6db01b6d,0xc0600003,0x86000d86,0x6db6c36d, - 0xddadb6ed,0x300001b6,0x6c360,0xe37236e4, - 0x46db6236,0xdb6c,0x361b018,0xb91b7200, - 0x6dbb1b71,0x6db763,0x20100820,0x61248001, - 0x92492490,0x24820004,0x8041000,0x92400090, - 0x24924830,0x555b6a49,0x2080012,0x20004804, - 0x49252449,0x84112492,0x4000928,0x240201, - 0x92922490,0x58924924,0x49456,0x120d8082, - 0x6da4800,0x69249249,0x249a01b,0x6c04100, - 0x6d240009,0x92492483,0x24d5adb4,0x60208001, - 0x92000483,0x24925236,0x6846da49,0x10400092, - 0x241b0,0x49291b49,0x636d2492,0x92494935, - 0x24924924,0x49249249,0x92492492,0x24924924, - 0x49249249,0x92492492,0x24924924,0x49249249, - 0x92492492,0x24924924,0x49249249,0x92492492, - 0x24924924,0x49249249,0x92492492,0x24924924, - 0x49249249,0x92492492,0x24924924,0x49249249, - 0x92492492,0x24924924,0x49249249,0x92492492, - 0x24924924,0x49249249,0x92492492,0x24924924, - 0x49249249,0x92492492,0x24924924,0x49249249, - 0x92492492,0x24924924,0x49249249,0x92492492, - 0x24924924,0x49249249,0x92492492,0x24924924, - 0x49249249,0x92492492,0x24924924,0x49249249, - 0x92492492,0x24924924,0x49249249,0x92492492, - 0x24924924,0x49249249,0x92492492,0x24924924, - 0x49249249,0x92492492,0x24924924,0x49249249, - 0x92492492,0x24924924,0x49249249,0x92492492, - 0x24924924,0x49249249,0x92492492,0x24924924, - 0x49249249,0x92492492,0x24924924, -]); - -class Lev1TParametricDescription extends ParametricDescription { - /** - * @param {number} absState - * @param {number} position - * @param {number} vector - * @returns {number} - */ - transition(absState, position, vector) { - let state = Math.floor(absState / (this.w + 1)); - let offset = absState % (this.w + 1); - - if (position === this.w) { - if (state < 2) { // eslint-disable-line no-lonely-if - const loc = Math.imul(vector, 2) + state; - offset += this.unpack(this.offsetIncrs0, loc, 1); - state = this.unpack(this.toStates0, loc, 2) - 1; - } - } else if (position === this.w - 1) { - if (state < 3) { // eslint-disable-line no-lonely-if - const loc = Math.imul(vector, 3) + state; - offset += this.unpack(this.offsetIncrs1, loc, 1); - state = this.unpack(this.toStates1, loc, 2) - 1; - } - } else if (position === this.w - 2) { - if (state < 6) { // eslint-disable-line no-lonely-if - const loc = Math.imul(vector, 6) + state; - offset += this.unpack(this.offsetIncrs2, loc, 2); - state = this.unpack(this.toStates2, loc, 3) - 1; +if (typeof window !== "undefined") { + const ROOT_PATH = window.rootPath; + /** @type {stringdex.Callbacks|null} */ + let databaseCallbacks = null; + initSearch(window.Stringdex, window.RoaringBitmap, { + loadRoot: callbacks => { + for (const key in callbacks) { + if (Object.hasOwn(callbacks, key)) { + // @ts-ignore + window[key] = callbacks[key]; + } } - } else { - if (state < 6) { // eslint-disable-line no-lonely-if - const loc = Math.imul(vector, 6) + state; - offset += this.unpack(this.offsetIncrs3, loc, 2); - state = this.unpack(this.toStates3, loc, 3) - 1; + databaseCallbacks = callbacks; + // search.index/root is loaded by main.js, so + // this script doesn't need to launch it, but + // must pick it up + // @ts-ignore + if (window.searchIndex) { + // @ts-ignore + window.rr_(window.searchIndex); } - } - - if (state === -1) { - // null state - return -1; - } else { - // translate back to abs - return Math.imul(state, this.w + 1) + offset; - } - } - - // state map - // 0 -> [(0, 0)] - // 1 -> [(0, 1)] - // 2 -> [(0, 1), (1, 1)] - // 3 -> [(0, 1), (1, 1), (2, 1)] - // 4 -> [(0, 1), (2, 1)] - // 5 -> [t(0, 1), (0, 1), (1, 1), (2, 1)] - - - /** @param {number} w - length of word being checked */ - constructor(w) { - super(w, 1, new Int32Array([0,1,0,-1,-1,-1])); - } -} - -Lev1TParametricDescription.prototype.toStates0 = /*2 bits per value */ new Int32Array([ - 0x2, -]); -Lev1TParametricDescription.prototype.offsetIncrs0 = /*1 bits per value */ new Int32Array([ - 0x0, -]); - -Lev1TParametricDescription.prototype.toStates1 = /*2 bits per value */ new Int32Array([ - 0xa43, -]); -Lev1TParametricDescription.prototype.offsetIncrs1 = /*1 bits per value */ new Int32Array([ - 0x38, -]); - -Lev1TParametricDescription.prototype.toStates2 = /*3 bits per value */ new Int32Array([ - 0x12180003,0xb45a4914,0x69, -]); -Lev1TParametricDescription.prototype.offsetIncrs2 = /*2 bits per value */ new Int32Array([ - 0x558a0000,0x5555, -]); - -Lev1TParametricDescription.prototype.toStates3 = /*3 bits per value */ new Int32Array([ - 0x900c0003,0xa1904864,0x45a49169,0x5a6d196a, - 0x9634, -]); -Lev1TParametricDescription.prototype.offsetIncrs3 = /*2 bits per value */ new Int32Array([ - 0xa0fc0000,0x5555ba08,0x55555555, -]); - -// ==================== -// WARNING: Nothing should be added below this comment: we need the `initSearch` function to -// be called ONLY when the whole file has been parsed and loaded. - -// @ts-expect-error -function initSearch(searchIndex) { - rawSearchIndex = searchIndex; - if (typeof window !== "undefined") { - // @ts-expect-error - docSearch = new DocSearch(rawSearchIndex, ROOT_PATH, searchState); - registerSearchEvents(); - // If there's a search term in the URL, execute the search now. - if (window.searchState.getQueryStringParams().search) { - search(); - } - } else if (typeof exports !== "undefined") { - // @ts-expect-error - docSearch = new DocSearch(rawSearchIndex, ROOT_PATH, searchState); - exports.docSearch = docSearch; - exports.parseQuery = DocSearch.parseQuery; - } -} - -if (typeof exports !== "undefined") { + }, + loadTreeByHash: hashHex => { + const script = document.createElement("script"); + script.src = `${ROOT_PATH}/search.index/${hashHex}.js`; + script.onerror = e => { + if (databaseCallbacks) { + databaseCallbacks.err_rn_(hashHex, e); + } + }; + document.documentElement.appendChild(script); + }, + loadDataByNameAndHash: (name, hashHex) => { + const script = document.createElement("script"); + script.src = `${ROOT_PATH}/search.index/${name}/${hashHex}.js`; + script.onerror = e => { + if (databaseCallbacks) { + databaseCallbacks.err_rd_(hashHex, e); + } + }; + document.documentElement.appendChild(script); + }, + }); +} else if (typeof exports !== "undefined") { + // eslint-disable-next-line no-undef exports.initSearch = initSearch; } - -if (typeof window !== "undefined") { - // @ts-expect-error - window.initSearch = initSearch; - // @ts-expect-error - if (window.searchIndex !== undefined) { - // @ts-expect-error - initSearch(window.searchIndex); - } -} else { - // Running in Node, not a browser. Run initSearch just to produce the - // exports. - initSearch(new Map()); -} diff --git a/src/librustdoc/html/static/js/settings.js b/src/librustdoc/html/static/js/settings.js index 2430b5829b2ba..01fd7a755ea87 100644 --- a/src/librustdoc/html/static/js/settings.js +++ b/src/librustdoc/html/static/js/settings.js @@ -1,25 +1,13 @@ // Local js definitions: /* global getSettingValue, updateLocalStorage, updateTheme */ /* global addClass, removeClass, onEach, onEachLazy */ -/* global MAIN_ID, getVar, getSettingsButton, getHelpButton, nonnull */ +/* global MAIN_ID, getVar, nonnull */ "use strict"; (function() { const isSettingsPage = window.location.pathname.endsWith("/settings.html"); - /** - * @param {Element} elem - * @param {EventTarget|null} target - */ - function elemContainsTarget(elem, target) { - if (target instanceof Node) { - return elem.contains(target); - } else { - return false; - } - } - /** * @overload {"theme"|"preferred-dark-theme"|"preferred-light-theme"} * @param {string} settingName @@ -305,10 +293,12 @@ } } else { el.setAttribute("tabindex", "-1"); - const settingsBtn = getSettingsButton(); - if (settingsBtn !== null) { - settingsBtn.appendChild(el); - } + onEachLazy(document.querySelectorAll(".settings-menu"), menu => { + if (menu.offsetWidth !== 0) { + menu.appendChild(el); + return true; + } + }); } return el; } @@ -317,6 +307,15 @@ function displaySettings() { settingsMenu.style.display = ""; + onEachLazy(document.querySelectorAll(".settings-menu"), menu => { + if (menu.offsetWidth !== 0) { + if (!menu.contains(settingsMenu) && settingsMenu.parentElement) { + settingsMenu.parentElement.removeChild(settingsMenu); + menu.appendChild(settingsMenu); + } + return true; + } + }); onEachLazy(settingsMenu.querySelectorAll("input[type='checkbox']"), el => { const val = getSettingValue(el.id); const checked = val === "true"; @@ -330,40 +329,37 @@ * @param {FocusEvent} event */ function settingsBlurHandler(event) { - const helpBtn = getHelpButton(); - const settingsBtn = getSettingsButton(); - const helpUnfocused = helpBtn === null || - (!helpBtn.contains(document.activeElement) && - !elemContainsTarget(helpBtn, event.relatedTarget)); - const settingsUnfocused = settingsBtn === null || - (!settingsBtn.contains(document.activeElement) && - !elemContainsTarget(settingsBtn, event.relatedTarget)); - if (helpUnfocused && settingsUnfocused) { + const isInPopover = onEachLazy( + document.querySelectorAll(".settings-menu, .help-menu"), + menu => { + return menu.contains(document.activeElement) || menu.contains(event.relatedTarget); + }, + ); + if (!isInPopover) { window.hidePopoverMenus(); } } if (!isSettingsPage) { // We replace the existing "onclick" callback. - // These elements must exist, as (outside of the settings page) - // `settings.js` is only loaded after the settings button is clicked. - const settingsButton = nonnull(getSettingsButton()); const settingsMenu = nonnull(document.getElementById("settings")); - settingsButton.onclick = event => { - if (elemContainsTarget(settingsMenu, event.target)) { - return; - } - event.preventDefault(); - const shouldDisplaySettings = settingsMenu.style.display === "none"; + onEachLazy(document.querySelectorAll(".settings-menu"), settingsButton => { + /** @param {MouseEvent} event */ + settingsButton.onclick = event => { + if (!(event.target instanceof Element) || settingsMenu.contains(event.target)) { + return; + } + event.preventDefault(); + const shouldDisplaySettings = settingsMenu.style.display === "none"; - window.hideAllModals(false); - if (shouldDisplaySettings) { - displaySettings(); - } - }; - settingsButton.onblur = settingsBlurHandler; - // the settings button should always have a link in it - nonnull(settingsButton.querySelector("a")).onblur = settingsBlurHandler; + window.hideAllModals(false); + if (shouldDisplaySettings) { + displaySettings(); + } + }; + settingsButton.onblur = settingsBlurHandler; + settingsButton.querySelector("a").onblur = settingsBlurHandler; + }); onEachLazy(settingsMenu.querySelectorAll("input"), el => { el.onblur = settingsBlurHandler; }); @@ -377,6 +373,8 @@ if (!isSettingsPage) { displaySettings(); } - removeClass(getSettingsButton(), "rotate"); + onEachLazy(document.querySelectorAll(".settings-menu"), settingsButton => { + removeClass(settingsButton, "rotate"); + }); }, 0); })(); diff --git a/src/librustdoc/html/static/js/storage.js b/src/librustdoc/html/static/js/storage.js index 7611372689478..6d745f4486e71 100644 --- a/src/librustdoc/html/static/js/storage.js +++ b/src/librustdoc/html/static/js/storage.js @@ -7,6 +7,7 @@ /** * @import * as rustdoc from "./rustdoc.d.ts"; + * @import * as stringdex from "./stringdex.d.ts"; */ const builtinThemes = ["light", "dark", "ayu"]; @@ -172,7 +173,7 @@ function updateLocalStorage(name, value) { } else { window.localStorage.setItem("rustdoc-" + name, value); } - } catch (e) { + } catch { // localStorage is not accessible, do nothing } } @@ -189,7 +190,7 @@ function updateLocalStorage(name, value) { function getCurrentValue(name) { try { return window.localStorage.getItem("rustdoc-" + name); - } catch (e) { + } catch { return null; } } @@ -375,32 +376,6 @@ window.addEventListener("pageshow", ev => { // That's also why this is in storage.js and not main.js. // // [parser]: https://html.spec.whatwg.org/multipage/parsing.html -class RustdocSearchElement extends HTMLElement { - constructor() { - super(); - } - connectedCallback() { - const rootPath = getVar("root-path"); - const currentCrate = getVar("current-crate"); - this.innerHTML = `<nav class="sub"> - <form class="search-form"> - <span></span> <!-- This empty span is a hacky fix for Safari - See #93184 --> - <div id="sidebar-button" tabindex="-1"> - <a href="${rootPath}${currentCrate}/all.html" title="show sidebar"></a> - </div> - <input - class="search-input" - name="search" - aria-label="Run search in the documentation" - autocomplete="off" - spellcheck="false" - placeholder="Type ‘S’ or ‘/’ to search, ‘?’ for more options…" - type="search"> - </form> - </nav>`; - } -} -window.customElements.define("rustdoc-search", RustdocSearchElement); class RustdocToolbarElement extends HTMLElement { constructor() { super(); @@ -411,14 +386,46 @@ class RustdocToolbarElement extends HTMLElement { return; } const rootPath = getVar("root-path"); + const currentUrl = window.location.href.split("?")[0].split("#")[0]; this.innerHTML = ` - <div id="settings-menu" tabindex="-1"> + <div id="search-button" tabindex="-1"> + <a href="${currentUrl}?search="><span class="label">Search</span></a> + </div> + <div class="settings-menu" tabindex="-1"> <a href="${rootPath}settings.html"><span class="label">Settings</span></a> </div> - <div id="help-button" tabindex="-1"> + <div class="help-menu" tabindex="-1"> <a href="${rootPath}help.html"><span class="label">Help</span></a> </div> <button id="toggle-all-docs"><span class="label">Summary</span></button>`; } } window.customElements.define("rustdoc-toolbar", RustdocToolbarElement); +class RustdocTopBarElement extends HTMLElement { + constructor() { + super(); + } + connectedCallback() { + const rootPath = getVar("root-path"); + const tmplt = document.createElement("template"); + tmplt.innerHTML = ` + <slot name="sidebar-menu-toggle"></slot> + <slot></slot> + <slot name="settings-menu"></slot> + <slot name="help-menu"></slot> + `; + const shadow = this.attachShadow({ mode: "open" }); + shadow.appendChild(tmplt.content.cloneNode(true)); + this.innerHTML += ` + <button class="sidebar-menu-toggle" slot="sidebar-menu-toggle" title="show sidebar"> + </button> + <div class="settings-menu" slot="settings-menu" tabindex="-1"> + <a href="${rootPath}settings.html"><span class="label">Settings</span></a> + </div> + <div class="help-menu" slot="help-menu" tabindex="-1"> + <a href="${rootPath}help.html"><span class="label">Help</span></a> + </div> + `; + } +} +window.customElements.define("rustdoc-topbar", RustdocTopBarElement); diff --git a/src/librustdoc/html/static/js/stringdex.d.ts b/src/librustdoc/html/static/js/stringdex.d.ts new file mode 100644 index 0000000000000..cd45f27667b8f --- /dev/null +++ b/src/librustdoc/html/static/js/stringdex.d.ts @@ -0,0 +1,164 @@ +export = stringdex; + +declare namespace stringdex { + /** + * The client interface to Stringdex. + */ + interface Database { + getIndex(colname: string): SearchTree|undefined; + getData(colname: string): DataColumn|undefined; + } + /** + * A search index file. + */ + interface SearchTree { + trie(): Trie; + search(name: Uint8Array|string): Promise<Trie?>; + searchLev(name: Uint8Array|string): AsyncGenerator<Trie>; + } + /** + * A compressed node in the search tree. + * + * This object logically addresses two interleaved trees: + * a "prefix tree", and a "suffix tree". If you ask for + * generic matches, you get both, but if you ask for one + * that excludes suffix-only entries, you'll get prefixes + * alone. + */ + interface Trie { + matches(): RoaringBitmap; + substringMatches(): AsyncGenerator<RoaringBitmap>; + prefixMatches(): AsyncGenerator<RoaringBitmap>; + keys(): Uint8Array; + keysExcludeSuffixOnly(): Uint8Array; + children(): [number, Promise<Trie>][]; + childrenExcludeSuffixOnly(): [number, Promise<Trie>][]; + child(id: number): Promise<Trie>?; + } + /** + * The client interface to Stringdex. + */ + interface DataColumn { + isEmpty(id: number): boolean; + at(id: number): Promise<Uint8Array|undefined>; + length: number, + } + /** + * Callbacks for a host application and VFS backend. + * + * These functions are calleb with mostly-raw data, + * except the JSONP wrapper is removed. For example, + * a file with the contents `rr_('{"A":"B"}')` should, + * after being pulled in, result in the `rr_` callback + * being invoked. + * + * The success callbacks don't need to supply the name of + * the file that succeeded, but, if you want successful error + * reporting, you'll need to remember which files are + * in flight and report the filename as the first parameter. + */ + interface Callbacks { + /** + * Load the root of the search database + * @param {string} dataString + */ + rr_: function(string); + err_rr_: function(any); + /** + * Load a nodefile in the search tree. + * A node file may contain multiple nodes; + * each node has five fields, separated by newlines. + * @param {string} inputBase64 + */ + rn_: function(string); + err_rn_: function(string, any); + /** + * Load a database column partition from a string + * @param {string} dataString + */ + rd_: function(string); + err_rd_: function(string, any); + /** + * Load a database column partition from base64 + * @param {string} dataString + */ + rb_: function(string); + err_rb_: function(string, any); + }; + /** + * Hooks that a VFS layer must provide for stringdex to load data. + * + * When the root is loaded, the Callbacks object is provided. These + * functions should result in callback functions being called with + * the contents of the file, or in error callbacks being invoked with + * the failed-to-load filename. + */ + interface Hooks { + /** + * The first function invoked as part of loading a search database. + * This function must, eventually, invoke `rr_` with the string + * representation of the root file (the function call wrapper, + * `rr_('` and `')`, must be removed). + * + * The supplied callbacks object is used to feed search data back + * to the search engine core. You have to store it, so that + * loadTreeByHash and loadDataByNameAndHash can use it. + * + * If this fails, either throw an exception, or call `err_rr_` + * with the error object. + */ + loadRoot: function(Callbacks); + /** + * Load a subtree file from the search index. + * + * If this function succeeds, call `rn_` on the callbacks + * object. If it fails, call `err_rn_(hashHex, error)`. + * + * @param {string} hashHex + */ + loadTreeByHash: function(string); + /** + * Load a column partition from the search database. + * + * If this function succeeds, call `rd_` or `rb_` on the callbacks + * object. If it fails, call `err_rd_(hashHex, error)`. or `err_rb_`. + * To determine which one, the wrapping function call in the js file + * specifies it. + * + * @param {string} columnName + * @param {string} hashHex + */ + loadDataByNameAndHash: function(string, string); + }; + class RoaringBitmap { + constructor(Uint8Array); + static makeSingleton(number: number); + static everything(): RoaringBitmap; + static empty(): RoaringBitmap; + isEmpty(): boolean; + union(that: RoaringBitmap): RoaringBitmap; + intersection(that: RoaringBitmap): RoaringBitmap; + contains(number: number): boolean; + entries(): Generator<number>; + first(): number|null; + }; + + type Stringdex = { + /** + * Initialize Stringdex with VFS hooks. + * Returns a database that you can use. + */ + loadDatabase: function(Hooks): Promise<Database>, + }; + + const Stringdex: Stringdex; + const RoaringBitmap: Class<stringdex.RoaringBitmap>; +} + +declare global { + interface Window { + Stringdex: stringdex.Stringdex; + RoaringBitmap: Class<stringdex.RoaringBitmap>; + StringdexOnload: Array<function(stringdex.Stringdex): any>?; + }; +} \ No newline at end of file diff --git a/src/librustdoc/html/static/js/stringdex.js b/src/librustdoc/html/static/js/stringdex.js new file mode 100644 index 0000000000000..b7bba8d329226 --- /dev/null +++ b/src/librustdoc/html/static/js/stringdex.js @@ -0,0 +1,2666 @@ +/** + * @import * as stringdex from "./stringdex.d.ts" + */ +/** + * @property {Array<number>} keys + * @property {Array<number>} cardinalities + * @property {Array<RoaringBitmapArray|RoaringBitmapBits|RoaringBitmapRun>} containers + */ +class RoaringBitmap { + /** @param {Uint8Array?} u8array */ + constructor(u8array) { + this.keys = []; + /** @type {number[]} */ + this.cardinalities = []; + /** @type {(RoaringBitmapArray|RoaringBitmapBits|RoaringBitmapRun)[]} */ + this.containers = []; + this.consumed_len_bytes = 0; + if (u8array === null || u8array.length === 0) { + return this; + } + if (u8array[0] < 0x3a) { + // Special representation of tiny sets + const lspecial = u8array[0]; + let pspecial = 1; + for (let ispecial = 0; ispecial < lspecial; ispecial += 1) { + const key = u8array[pspecial + 2] | (u8array[pspecial + 3] << 8); + const value = u8array[pspecial] | (u8array[pspecial + 1] << 8); + let mid = this.getContainerId(key); + /** @type {RoaringBitmapArray|RoaringBitmapBits|RoaringBitmapRun} */ + let container; + if (mid === -1) { + this.keys.push(key); + container = new RoaringBitmapArray(0, new Uint8Array(2)); + mid = this.containers.length; + this.containers.push(container); + this.cardinalities.push(0); + } else { + container = this.containers[mid]; + } + const cardinalityOld = this.cardinalities[mid]; + if (!(container instanceof RoaringBitmapArray) || + container.array.byteLength < ((cardinalityOld + 1) * 2) + ) { + const newBuf = new Uint8Array((cardinalityOld + 1) * 4); + let idx = 0; + for (const cvalue of container.values()) { + newBuf[idx] = cvalue & 0xFF; + newBuf[idx + 1] = (cvalue >> 8) & 0xFF; + idx += 2; + } + container = new RoaringBitmapArray(cardinalityOld + 1, newBuf); + this.containers[mid] = container; + } else { + container.cardinality = cardinalityOld + 1; + } + container.array[cardinalityOld * 2] = value & 0xFF; + container.array[(cardinalityOld * 2) + 1] = (value >> 8) & 0xFF; + this.cardinalities[mid] = cardinalityOld + 1; + pspecial += 4; + } + this.consumed_len_bytes = pspecial; + return this; + } + // https://github.com/RoaringBitmap/RoaringFormatSpec + // + // Roaring bitmaps are used for flags that can be kept in their + // compressed form, even when loaded into memory. This decoder + // turns the containers into objects, but uses byte array + // slices of the original format for the data payload. + const has_runs = u8array[0] === 0x3b; + if (u8array[0] !== 0x3a && u8array[0] !== 0x3b) { + throw new Error("not a roaring bitmap: " + u8array[0]); + } + const size = has_runs ? + ((u8array[2] | (u8array[3] << 8)) + 1) : + ((u8array[4] | (u8array[5] << 8) | (u8array[6] << 16) | (u8array[7] << 24))); + let i = has_runs ? 4 : 8; + let is_run; + if (has_runs) { + const is_run_len = Math.floor((size + 7) / 8); + is_run = new Uint8Array(u8array.buffer, i + u8array.byteOffset, is_run_len); + i += is_run_len; + } else { + is_run = new Uint8Array(); + } + for (let j = 0; j < size; ++j) { + this.keys.push(u8array[i] | (u8array[i + 1] << 8)); + i += 2; + this.cardinalities.push((u8array[i] | (u8array[i + 1] << 8)) + 1); + i += 2; + } + let offsets = null; + if (!has_runs || this.keys.length >= 4) { + offsets = []; + for (let j = 0; j < size; ++j) { + offsets.push(u8array[i] | (u8array[i + 1] << 8) | (u8array[i + 2] << 16) | + (u8array[i + 3] << 24)); + i += 4; + } + } + for (let j = 0; j < size; ++j) { + if (offsets && offsets[j] !== i) { + throw new Error(`corrupt bitmap ${j}: ${i} / ${offsets[j]}`); + } + if (is_run[j >> 3] & (1 << (j & 0x7))) { + const runcount = (u8array[i] | (u8array[i + 1] << 8)); + i += 2; + this.containers.push(new RoaringBitmapRun( + runcount, + new Uint8Array(u8array.buffer, i + u8array.byteOffset, runcount * 4), + )); + i += runcount * 4; + } else if (this.cardinalities[j] >= 4096) { + this.containers.push(new RoaringBitmapBits(new Uint8Array( + u8array.buffer, + i + u8array.byteOffset, 8192, + ))); + i += 8192; + } else { + const end = this.cardinalities[j] * 2; + this.containers.push(new RoaringBitmapArray( + this.cardinalities[j], + new Uint8Array(u8array.buffer, i + u8array.byteOffset, end), + )); + i += end; + } + } + this.consumed_len_bytes = i; + } + /** + * @param {number} number + * @returns {RoaringBitmap} + */ + static makeSingleton(number) { + const result = new RoaringBitmap(null); + result.cardinalities.push(1); + result.keys.push(number >> 16); + result.containers.push(new RoaringBitmapArray( + 1, + Uint8Array.from([number, number >> 8]), + )); + return result; + } + /** @returns {RoaringBitmap} */ + static everything() { + if (EVERYTHING_BITMAP.isEmpty()) { + let i = 0; + const l = 1 << 16; + const everything_range = new RoaringBitmapRun(1, Uint8Array.of(0, 0, 0xff, 0xff)); + while (i < l) { + EVERYTHING_BITMAP.keys.push(i); + EVERYTHING_BITMAP.cardinalities.push(1 << 16); + EVERYTHING_BITMAP.containers.push(everything_range); + i += 1; + } + } + return EVERYTHING_BITMAP; + } + /** @returns {RoaringBitmap} */ + static empty() { + return EMPTY_BITMAP; + } + /** @returns {boolean} */ + isEmpty() { + return this.containers.length === 0; + } + /** + * @param {RoaringBitmap} that + * @returns {RoaringBitmap} + */ + union(that) { + if (this.isEmpty()) { + return that; + } + if (that.isEmpty()) { + return this; + } + if (this === RoaringBitmap.everything() || that === RoaringBitmap.everything()) { + return RoaringBitmap.everything(); + } + let i = 0; + const il = this.keys.length; + let j = 0; + const jl = that.keys.length; + const result = new RoaringBitmap(null); + while (i < il || j < jl) { + if (j >= jl || this.keys[i] < that.keys[j]) { + result.keys.push(this.keys[i]); + result.containers.push(this.containers[i]); + result.cardinalities.push(this.cardinalities[i]); + i += 1; + } else if (i >= il || that.keys[j] < this.keys[i]) { + result.keys.push(that.keys[j]); + result.containers.push(that.containers[j]); + result.cardinalities.push(that.cardinalities[j]); + j += 1; + } else { + // this key is not smaller than that key + // that key is not smaller than this key + // they must be equal + result.keys.push(this.keys[i]); + const thisContainer = this.containers[i]; + const thatContainer = that.containers[j]; + if (thisContainer instanceof RoaringBitmapBits && + thatContainer instanceof RoaringBitmapBits + ) { + const resultArray = new Uint8Array( + thisContainer.array.length > thatContainer.array.length ? + thisContainer.array.length : + thatContainer.array.length, + ); + let k = 0; + const kl = resultArray.length; + let card = 0; + while (k < kl) { + const c = thisContainer.array[k] | thatContainer.array[k]; + resultArray[k] = c; + card += bitCount(c); + k += 1; + } + result.containers.push(new RoaringBitmapBits(resultArray)); + result.cardinalities.push(card); + } else { + const thisValues = thisContainer.values(); + const thatValues = thatContainer.values(); + let thisResult = thisValues.next(); + let thatResult = thatValues.next(); + /** @type {Array<number>} */ + const resultValues = []; + while (!thatResult.done || !thisResult.done) { + // generator will definitely implement the iterator protocol correctly + /** @type {number} */ + const thisValue = thisResult.value; + /** @type {number} */ + const thatValue = thatResult.value; + if (thatResult.done || thisValue < thatValue) { + resultValues.push(thisValue); + thisResult = thisValues.next(); + } else if (thisResult.done || thatValue < thisValue) { + resultValues.push(thatValue); + thatResult = thatValues.next(); + } else { + // this value is not smaller than that value + // that value is not smaller than this value + // they must be equal + resultValues.push(thisValue); + thisResult = thisValues.next(); + thatResult = thatValues.next(); + } + } + const resultArray = new Uint8Array(resultValues.length * 2); + let k = 0; + for (const value of resultValues) { + // roaring bitmap is little endian + resultArray[k] = value & 0xFF; + resultArray[k + 1] = (value >> 8) & 0xFF; + k += 2; + } + result.containers.push(new RoaringBitmapArray( + resultValues.length, + resultArray, + )); + result.cardinalities.push(resultValues.length); + } + i += 1; + j += 1; + } + } + return result; + } + /** + * @param {RoaringBitmap} that + * @returns {RoaringBitmap} + */ + intersection(that) { + if (this.isEmpty() || that.isEmpty()) { + return EMPTY_BITMAP; + } + if (this === RoaringBitmap.everything()) { + return that; + } + if (that === RoaringBitmap.everything()) { + return this; + } + let i = 0; + const il = this.keys.length; + let j = 0; + const jl = that.keys.length; + const result = new RoaringBitmap(null); + while (i < il && j < jl) { + if (this.keys[i] < that.keys[j]) { + i += 1; + } else if (that.keys[j] < this.keys[i]) { + j += 1; + } else { + // this key is not smaller than that key + // that key is not smaller than this key + // they must be equal + const thisContainer = this.containers[i]; + const thatContainer = that.containers[j]; + if (thisContainer instanceof RoaringBitmapBits && + thatContainer instanceof RoaringBitmapBits + ) { + const resultArray = new Uint8Array( + thisContainer.array.length > thatContainer.array.length ? + thisContainer.array.length : + thatContainer.array.length, + ); + let k = 0; + const kl = resultArray.length; + let card = 0; + while (k < kl) { + const c = thisContainer.array[k] & thatContainer.array[k]; + resultArray[k] = c; + card += bitCount(c); + k += 1; + } + if (card !== 0) { + result.keys.push(this.keys[i]); + result.containers.push(new RoaringBitmapBits(resultArray)); + result.cardinalities.push(card); + } + } else { + const thisValues = thisContainer.values(); + const thatValues = thatContainer.values(); + let thisValue = thisValues.next(); + let thatValue = thatValues.next(); + const resultValues = []; + while (!thatValue.done && !thisValue.done) { + if (thisValue.value < thatValue.value) { + thisValue = thisValues.next(); + } else if (thatValue.value < thisValue.value) { + thatValue = thatValues.next(); + } else { + // this value is not smaller than that value + // that value is not smaller than this value + // they must be equal + resultValues.push(thisValue.value); + thisValue = thisValues.next(); + thatValue = thatValues.next(); + } + } + if (resultValues.length !== 0) { + const resultArray = new Uint8Array(resultValues.length * 2); + let k = 0; + for (const value of resultValues) { + // roaring bitmap is little endian + resultArray[k] = value & 0xFF; + resultArray[k + 1] = (value >> 8) & 0xFF; + k += 2; + } + result.keys.push(this.keys[i]); + result.containers.push(new RoaringBitmapArray( + resultValues.length, + resultArray, + )); + result.cardinalities.push(resultValues.length); + } + } + i += 1; + j += 1; + } + } + return result; + } + /** @param {number} keyvalue */ + contains(keyvalue) { + const key = keyvalue >> 16; + const value = keyvalue & 0xFFFF; + const mid = this.getContainerId(key); + return mid === -1 ? false : this.containers[mid].contains(value); + } + /** + * @param {number} key + * @returns {number} + */ + getContainerId(key) { + // Binary search algorithm copied from + // https://en.wikipedia.org/wiki/Binary_search#Procedure + // + // Format is required by specification to be sorted. + // Because keys are 16 bits and unique, length can't be + // bigger than 2**16, and because we have 32 bits of safe int, + // left + right can't overflow. + let left = 0; + let right = this.keys.length - 1; + while (left <= right) { + const mid = Math.floor((left + right) / 2); + const x = this.keys[mid]; + if (x < key) { + left = mid + 1; + } else if (x > key) { + right = mid - 1; + } else { + return mid; + } + } + return -1; + } + * entries() { + for (const i in this.containers) { + if (!Object.prototype.hasOwnProperty.call(this.containers, i)) { + continue; + } + const key = this.keys[i]; + for (const value of this.containers[i].values()) { + yield (key << 16) | value; + } + } + } + /** + * @returns {number|null} + */ + first() { + for (const entry of this.entries()) { + return entry; + } + return null; + } + /** + * @returns {number} + */ + cardinality() { + let result = 0; + for (const card of this.cardinalities) { + result += card; + } + return result; + } +} + +class RoaringBitmapRun { + /** + * @param {number} runcount + * @param {Uint8Array} array + */ + constructor(runcount, array) { + this.runcount = runcount; + this.array = array; + } + /** @param {number} value */ + contains(value) { + // Binary search algorithm copied from + // https://en.wikipedia.org/wiki/Binary_search#Procedure + // + // Since runcount is stored as 16 bits, left + right + // can't overflow. + let left = 0; + let right = this.runcount - 1; + while (left <= right) { + const mid = Math.floor((left + right) / 2); + const i = mid * 4; + const start = this.array[i] | (this.array[i + 1] << 8); + const lenm1 = this.array[i + 2] | (this.array[i + 3] << 8); + if ((start + lenm1) < value) { + left = mid + 1; + } else if (start > value) { + right = mid - 1; + } else { + return true; + } + } + return false; + } + * values() { + let i = 0; + while (i < this.runcount) { + const start = this.array[i * 4] | (this.array[(i * 4) + 1] << 8); + const lenm1 = this.array[(i * 4) + 2] | (this.array[(i * 4) + 3] << 8); + let value = start; + let j = 0; + while (j <= lenm1) { + yield value; + value += 1; + j += 1; + } + i += 1; + } + } +} +class RoaringBitmapArray { + /** + * @param {number} cardinality + * @param {Uint8Array} array + */ + constructor(cardinality, array) { + this.cardinality = cardinality; + this.array = array; + } + /** @param {number} value */ + contains(value) { + // Binary search algorithm copied from + // https://en.wikipedia.org/wiki/Binary_search#Procedure + // + // Since cardinality can't be higher than 4096, left + right + // cannot overflow. + let left = 0; + let right = this.cardinality - 1; + while (left <= right) { + const mid = Math.floor((left + right) / 2); + const i = mid * 2; + const x = this.array[i] | (this.array[i + 1] << 8); + if (x < value) { + left = mid + 1; + } else if (x > value) { + right = mid - 1; + } else { + return true; + } + } + return false; + } + /** @returns {Generator<number>} */ + * values() { + let i = 0; + const l = this.cardinality * 2; + while (i < l) { + yield this.array[i] | (this.array[i + 1] << 8); + i += 2; + } + } +} +class RoaringBitmapBits { + /** + * @param {Uint8Array} array + */ + constructor(array) { + this.array = array; + } + /** @param {number} value */ + contains(value) { + return !!(this.array[value >> 3] & (1 << (value & 7))); + } + * values() { + let i = 0; + const l = this.array.length << 3; + while (i < l) { + if (this.contains(i)) { + yield i; + } + i += 1; + } + } +} + +const EMPTY_BITMAP = new RoaringBitmap(null); +EMPTY_BITMAP.consumed_len_bytes = 0; +const EMPTY_BITMAP1 = new RoaringBitmap(null); +EMPTY_BITMAP1.consumed_len_bytes = 1; +const EVERYTHING_BITMAP = new RoaringBitmap(null); + +const EMPTY_UINT8 = new Uint8Array(); + +/** + * A mapping from six byte nodeids to an arbitrary value. + * We don't just use `Map` because that requires double hashing. + * @template T + * @property {Uint8Array} keys + * @property {T[]} values + * @property {number} size + * @property {number} capacityClass + */ +class HashTable { + /** + * Construct an empty hash table. + */ + constructor() { + this.keys = EMPTY_UINT8; + /** @type {(T|undefined)[]} */ + this.values = []; + this.size = 0; + this.capacityClass = 0; + } + /** + * @returns {Generator<[Uint8Array, T]>} + */ + * entries() { + const keys = this.keys; + const values = this.values; + const l = this.values.length; + for (let i = 0; i < l; i += 1) { + const value = values[i]; + if (value !== undefined) { + yield [keys.subarray(i * 6, (i + 1) * 6), value]; + } + } + } + /** + * Add a value to the hash table. + * @param {Uint8Array} key + * @param {T} value + */ + set(key, value) { + // 90 % load factor + if (this.size * 10 >= this.values.length * 9) { + const keys = this.keys; + const values = this.values; + const l = values.length; + this.capacityClass += 1; + const capacity = 1 << this.capacityClass; + this.keys = new Uint8Array(capacity * 6); + this.values = []; + for (let i = 0; i < capacity; i += 1) { + this.values.push(undefined); + } + this.size = 0; + for (let i = 0; i < l; i += 1) { + const oldValue = values[i]; + if (oldValue !== undefined) { + this.setNoGrow(keys.subarray(i * 6, (i + 1) * 6), oldValue); + } + } + } + this.setNoGrow(key, value); + } + /** + * @param {Uint8Array} key + * @param {T} value + */ + setNoGrow(key, value) { + if (key.length !== 6) { + throw "invalid key"; + } + const mask = ~(0xffffffff << this.capacityClass); + const keys = this.keys; + const values = this.values; + const l = 1 << this.capacityClass; + // because we know that our values are already hashed, + // just chop off the lower four bytes + let slot = ((key[2] << 24) | (key[3] << 16) | (key[4] << 8) | key[5]) & mask; + for (let distance = 0; distance < l; ) { + const j = slot * 6; + const otherValue = values[slot]; + if (otherValue === undefined) { + values[slot] = value; + keys.set(key, slot * 6); + this.size += 1; + break; + } else if ( + key[0] === keys[j + 0] && + key[1] === keys[j + 1] && + key[2] === keys[j + 2] && + key[3] === keys[j + 3] && + key[4] === keys[j + 4] && + key[5] === keys[j + 5] + ) { + values[slot] = value; + break; + } else { + const otherPreferredSlot = ( + (keys[j + 2] << 24) | (keys[j + 3] << 16) | + (keys[j + 4] << 8) | keys[j + 5] + ) & mask; + const otherDistance = otherPreferredSlot <= slot ? + slot - otherPreferredSlot : + (l - otherPreferredSlot) + slot; + if (distance > otherDistance) { + // if the other key is closer to its preferred slot than this one, + // then insert our node in its place and swap + // + // https://cglab.ca/~abeinges/blah/robinhood-part-1/ + const otherKey = keys.slice(j, j + 6); + values[slot] = value; + value = otherValue; + keys.set(key, j); + key = otherKey; + distance = otherDistance; + } + distance += 1; + slot = (slot + 1) & mask; + } + } + } + /** + * Retrieve a value + * @param {Uint8Array} key + * @returns {T|undefined} + */ + get(key) { + if (key.length !== 6) { + throw "invalid key"; + } + const mask = ~(0xffffffff << this.capacityClass); + const keys = this.keys; + const values = this.values; + const l = 1 << this.capacityClass; + // because we know that our values are already hashed, + // just chop off the lower four bytes + let slot = ((key[2] << 24) | (key[3] << 16) | (key[4] << 8) | key[5]) & mask; + for (let distance = 0; distance < l; distance += 1) { + const j = slot * 6; + const value = values[slot]; + if (value === undefined) { + break; + } else if ( + key[0] === keys[j + 0] && + key[1] === keys[j + 1] && + key[2] === keys[j + 2] && + key[3] === keys[j + 3] && + key[4] === keys[j + 4] && + key[5] === keys[j + 5] + ) { + return value; + } else { + const otherPreferredSlot = ( + (keys[j + 2] << 24) | (keys[j + 3] << 16) | + (keys[j + 4] << 8) | keys[j + 5] + ) & mask; + const otherDistance = otherPreferredSlot <= slot ? + slot - otherPreferredSlot : + (l - otherPreferredSlot) + slot; + if (distance > otherDistance) { + break; + } + } + slot = (slot + 1) & mask; + } + return undefined; + } +} + +/*eslint-disable */ +// ignore-tidy-linelength +/** <https://stackoverflow.com/questions/43122082/efficiently-count-the-number-of-bits-in-an-integer-in-javascript> + * @param {number} n + * @returns {number} + */ +function bitCount(n) { + n = (~~n) - ((n >> 1) & 0x55555555); + n = (n & 0x33333333) + ((n >> 2) & 0x33333333); + return ((n + (n >> 4) & 0xF0F0F0F) * 0x1010101) >> 24; +} +/*eslint-enable */ + +/** + * @param {stringdex.Hooks} hooks + * @returns {Promise<stringdex.Database>} + */ +function loadDatabase(hooks) { + /** @type {stringdex.Callbacks} */ + const callbacks = { + rr_: function(data) { + const dataObj = JSON.parse(data); + for (const colName of Object.keys(dataObj)) { + if (Object.hasOwn(dataObj[colName], "I")) { + registry.searchTreeRoots.set( + colName, + makeSearchTreeFromBase64(dataObj[colName].I)[1], + ); + } + if (Object.hasOwn(dataObj[colName], "N")) { + const counts = []; + const countsstring = dataObj[colName]["N"]; + let i = 0; + const l = countsstring.length; + while (i < l) { + let n = 0; + let c = countsstring.charCodeAt(i); + while (c < 96) { // 96 = "`" + n = (n << 4) | (c & 0xF); + i += 1; + c = countsstring.charCodeAt(i); + } + n = (n << 4) | (c & 0xF); + counts.push(n); + i += 1; + } + registry.dataColumns.set(colName, new DataColumn( + counts, + makeUint8ArrayFromBase64(dataObj[colName]["H"]), + new RoaringBitmap(makeUint8ArrayFromBase64(dataObj[colName]["E"])), + colName, + )); + } + } + const cb = registry.searchTreeRootCallback; + if (cb) { + cb(null, new Database(registry.searchTreeRoots, registry.dataColumns)); + } + }, + err_rr_: function(err) { + const cb = registry.searchTreeRootCallback; + if (cb) { + cb(err, null); + } + }, + rd_: function(dataString) { + const l = dataString.length; + const data = new Uint8Array(l); + for (let i = 0; i < l; ++i) { + data[i] = dataString.charCodeAt(i); + } + loadColumnFromBytes(data); + }, + err_rd_: function(filename, err) { + const hash = makeUint8ArrayFromHex(filename); + const cb = registry.dataColumnLoadPromiseCallbacks.get(hash); + if (cb) { + cb(err, null); + } + }, + rb_: function(dataString64) { + loadColumnFromBytes(makeUint8ArrayFromBase64(dataString64)); + }, + err_rb_: function(filename, err) { + const hash = makeUint8ArrayFromHex(filename); + const cb = registry.dataColumnLoadPromiseCallbacks.get(hash); + if (cb) { + cb(err, null); + } + }, + rn_: function(inputBase64) { + const [hash, tree] = makeSearchTreeFromBase64(inputBase64); + const cb = registry.searchTreeLoadPromiseCallbacks.get(hash); + if (cb) { + cb(null, tree); + registry.searchTreeLoadPromiseCallbacks.set(hash, null); + } + }, + err_rn_: function(filename, err) { + const hash = makeUint8ArrayFromHex(filename); + const cb = registry.searchTreeLoadPromiseCallbacks.get(hash); + if (cb) { + cb(err, null); + } + }, + }; + + /** @type {{ + * searchTreeRoots: Map<string, SearchTree>; + * searchTreeLoadPromiseCallbacks: HashTable<(function(any, SearchTree?): any)|null>; + * searchTreeLoadByHash: function(Uint8Array): Promise<SearchTree>; + * searchTreeRootCallback?: function(any, Database?): any; + * dataColumns: Map<string, DataColumn>; + * dataColumnLoadPromiseCallbacks: HashTable<function(any, Uint8Array[]?): any>; + * dataLoadByNameAndHash: function(string, Uint8Array): Promise<Uint8Array[]>; + * }} */ + const registry = { + searchTreeLoadPromiseCallbacks: new HashTable(), + dataColumnLoadPromiseCallbacks: new HashTable(), + searchTreeRoots: new Map(), + dataColumns: new Map(), + searchTreeLoadByHash: function(hash) { + if ((hash[0] & 0x80) !== 0) { + const isWhole = (hash[0] & 0x40) !== 0; + const leaves = (hash[0] & 0x10) !== 0 ? + RoaringBitmap.makeSingleton((hash[4] << 8) | hash[5]) + .union(RoaringBitmap.makeSingleton((hash[2] << 8) | hash[3])) : + RoaringBitmap.makeSingleton( + (hash[2] << 24) | (hash[3] << 16) | + (hash[4] << 8) | hash[5], + ); + const data = (hash[0] & 0x01) !== 0 ? + Uint8Array.of(hash[1]) : + EMPTY_UINT8; + return Promise.resolve(new SearchTree( + EMPTY_UINT8, + EMPTY_UINT8, + EMPTY_UINT8, + EMPTY_UINT8, + data, + isWhole ? leaves : EMPTY_BITMAP, + isWhole ? EMPTY_BITMAP : leaves, + )); + } else { + const hashHex = makeHexFromUint8Array(hash); + return new Promise((resolve, reject) => { + const cb = registry.searchTreeLoadPromiseCallbacks.get(hash); + if (cb) { + registry.searchTreeLoadPromiseCallbacks.set(hash, (err, data) => { + cb(err, data); + if (data) { + resolve(data); + } else { + reject(err); + } + }); + } else { + registry.searchTreeLoadPromiseCallbacks.set(hash, (err, data) => { + if (data) { + resolve(data); + } else { + reject(err); + } + }); + } + hooks.loadTreeByHash(hashHex); + }); + } + }, + dataLoadByNameAndHash: function(name, hash) { + const hashHex = makeHexFromUint8Array(hash); + return new Promise((resolve, reject) => { + const cb = registry.dataColumnLoadPromiseCallbacks.get(hash); + if (cb) { + registry.dataColumnLoadPromiseCallbacks.set(hash, (err, data) => { + cb(err, data); + if (data) { + resolve(data); + } else { + reject(err); + } + }); + } else { + registry.dataColumnLoadPromiseCallbacks.set(hash, (err, data) => { + if (data) { + resolve(data); + } else { + reject(err); + } + }); + } + hooks.loadDataByNameAndHash(name, hashHex); + }); + }, + }; + + /** + * A sorted array of search tree branches. + * + * @type {{ + * keys: Uint8Array, + * hashes: Uint8Array, + * subtrees: Array<Promise<SearchTree>|null>, + * }} + */ + class SearchTreeBranches { + /** + * @param {Uint8Array} keys + * @param {Uint8Array} hashes + */ + constructor(keys, hashes) { + this.keys = keys; + this.hashes = hashes; + this.subtrees = []; + const length = keys.length; + for (let i = 0; i < length; ++i) { + this.subtrees.push(null); + } + let i = 1; + while (i < this.keys.length) { + if (this.keys[i - 1] >= this.keys[i]) { + throw new Error("HERE"); + } + i += 1; + } + } + /** @returns {Generator<[number, Promise<SearchTree>|null]>} */ + * entries() { + let i = 0; + const l = this.keys.length; + while (i < l) { + yield [this.keys[i], this.subtrees[i]]; + i += 1; + } + } + /** + * @param {number} k + * @returns {number} + */ + getIndex(k) { + // Since length can't be bigger than 256, + // left + right can't overflow. + let left = 0; + let right = this.keys.length - 1; + while (left <= right) { + const mid = Math.floor((left + right) / 2); + if (this.keys[mid] < k) { + left = mid + 1; + } else if (this.keys[mid] > k) { + right = mid - 1; + } else { + return mid; + } + } + return -1; + } + /** + * @param {number} i + * @returns {Uint8Array} + */ + getHash(i) { + return new Uint8Array( + this.hashes.buffer, + this.hashes.byteOffset + (i * 6), + 6, + ); + } + } + + /** + * A [suffix tree], used for name-based search. + * + * This data structure is used to drive substring matches, + * such as matching the query "link" to `LinkedList`, + * and Lev-distance matches, such as matching the + * query "hahsmap" to `HashMap`. + * + * [suffix tree]: https://en.wikipedia.org/wiki/Suffix_tree + * + * branches + * : A sorted-array map of subtrees. + * + * data + * : The substring represented by this node. The root node + * is always empty. + * + * leaves_suffix + * : The IDs of every entry that matches. Levenshtein matches + * won't include these. + * + * leaves_whole + * : The IDs of every entry that matches exactly. Levenshtein matches + * will include these. + * + * @type {{ + * might_have_prefix_branches: SearchTreeBranches, + * branches: SearchTreeBranches, + * data: Uint8Array, + * leaves_suffix: RoaringBitmap, + * leaves_whole: RoaringBitmap, + * }} + */ + class SearchTree { + /** + * @param {Uint8Array} might_have_prefix_branch_keys + * @param {Uint8Array} might_have_prefix_branch_hashes + * @param {Uint8Array} suffix_only_branch_keys + * @param {Uint8Array} suffix_only_branch_hashes + * @param {Uint8Array} data + * @param {RoaringBitmap} leaves_whole + * @param {RoaringBitmap} leaves_suffix + */ + constructor( + might_have_prefix_branch_keys, + might_have_prefix_branch_hashes, + suffix_only_branch_keys, + suffix_only_branch_hashes, + data, + leaves_whole, + leaves_suffix, + ) { + this.might_have_prefix_branches = new SearchTreeBranches( + might_have_prefix_branch_keys, + might_have_prefix_branch_hashes, + ); + if (suffix_only_branch_keys.length === 0) { + this.branches = this.might_have_prefix_branches; + } else if (might_have_prefix_branch_keys.length === 0) { + this.branches = new SearchTreeBranches( + suffix_only_branch_keys, + suffix_only_branch_hashes, + ); + } else { + const total_length = might_have_prefix_branch_keys.length + + suffix_only_branch_keys.length; + const keys = new Uint8Array(total_length); + const hashes = new Uint8Array(total_length * 6); + let mhpi = 0; + const mhpl = might_have_prefix_branch_keys.length; + const sol = suffix_only_branch_keys.length; + let soi = 0; + let i = 0; + while (i < total_length) { + if (soi >= sol || ( + mhpi < mhpl && + might_have_prefix_branch_keys[mhpi] < suffix_only_branch_keys[soi] + )) { + keys[i] = might_have_prefix_branch_keys[mhpi]; + hashes.set(might_have_prefix_branch_hashes.subarray( + mhpi * 6, + (mhpi + 1) * 6, + ), i * 6); + mhpi += 1; + } else { + keys[i] = suffix_only_branch_keys[soi]; + hashes.set(suffix_only_branch_hashes.subarray( + soi * 6, + (soi + 1) * 6, + ), i * 6); + soi += 1; + } + i += 1; + } + this.branches = new SearchTreeBranches( + keys, + hashes, + ); + } + this.data = data; + this.leaves_suffix = leaves_suffix; + this.leaves_whole = leaves_whole; + } + /** + * Returns the Trie for the root node. + * + * A Trie pointer refers to a single node in a logical decompressed search tree + * (the real search tree is compressed). + * + * @return {Trie} + */ + trie() { + return new Trie(this, 0); + } + + /** + * Return the trie representing `name` + * @param {Uint8Array|string} name + * @returns {Promise<Trie?>} + */ + async search(name) { + if (typeof name === "string") { + const utf8encoder = new TextEncoder(); + name = utf8encoder.encode(name); + } + let trie = this.trie(); + for (const datum of name) { + // code point definitely exists + const newTrie = trie.child(datum); + if (newTrie) { + trie = await newTrie; + } else { + return null; + } + } + return trie; + } + + /** + * @param {Uint8Array|string} name + * @returns {AsyncGenerator<Trie>} + */ + async* searchLev(name) { + if (typeof name === "string") { + const utf8encoder = new TextEncoder(); + name = utf8encoder.encode(name); + } + const w = name.length; + if (w < 3) { + const trie = await this.search(name); + if (trie !== null) { + yield trie; + } + return; + } + const levParams = w >= 6 ? + new Lev2TParametricDescription(w) : + new Lev1TParametricDescription(w); + /** @type {Array<[Promise<Trie>, number]>} */ + const stack = [[Promise.resolve(this.trie()), 0]]; + const n = levParams.n; + while (stack.length !== 0) { + // It's not empty + /** @type {[Promise<Trie>, number]} */ + //@ts-expect-error + const [triePromise, levState] = stack.pop(); + const trie = await triePromise; + for (const byte of trie.keysExcludeSuffixOnly()) { + const levPos = levParams.getPosition(levState); + const vector = levParams.getVector( + name, + byte, + levPos, + Math.min(w, levPos + (2 * n) + 1), + ); + const newLevState = levParams.transition( + levState, + levPos, + vector, + ); + if (newLevState >= 0) { + const child = trie.child(byte); + if (child) { + stack.push([child, newLevState]); + if (levParams.isAccept(newLevState)) { + yield child; + } + } + } + } + } + } + } + + /** + * A representation of a set of strings in the search index, + * as a subset of the entire tree. + */ + class Trie { + /** + * @param {SearchTree} tree + * @param {number} offset + */ + constructor(tree, offset) { + this.tree = tree; + this.offset = offset; + } + + /** + * All exact matches for the string represented by this node. + * @returns {RoaringBitmap} + */ + matches() { + if (this.offset === this.tree.data.length) { + return this.tree.leaves_whole; + } else { + return EMPTY_BITMAP; + } + } + + /** + * All matches for strings that contain the string represented by this node. + * @returns {AsyncGenerator<RoaringBitmap>} + */ + async* substringMatches() { + /** @type {Promise<SearchTree>[]} */ + let layer = [Promise.resolve(this.tree)]; + while (layer.length) { + const current_layer = layer; + layer = []; + for await (const tree of current_layer) { + yield tree.leaves_whole.union(tree.leaves_suffix); + } + /** @type {HashTable<[number, SearchTree][]>} */ + const subnodes = new HashTable(); + for await (const node of current_layer) { + const branches = node.branches; + const l = branches.subtrees.length; + for (let i = 0; i < l; ++i) { + const subtree = branches.subtrees[i]; + if (subtree) { + layer.push(subtree); + } else if (subtree === null) { + const byte = branches.keys[i]; + const newnode = branches.getHash(i); + if (!newnode) { + throw new Error(`malformed tree; no hash for key ${byte}`); + } else { + let subnode_list = subnodes.get(newnode); + if (!subnode_list) { + subnode_list = [[byte, node]]; + subnodes.set(newnode, subnode_list); + } else { + subnode_list.push([byte, node]); + } + } + } else { + throw new Error(`malformed tree; index ${i} does not exist`); + } + } + } + for (const [newnode, subnode_list] of subnodes.entries()) { + const res = registry.searchTreeLoadByHash(newnode); + for (const [byte, node] of subnode_list) { + const branches = node.branches; + const might_have_prefix_branches = node.might_have_prefix_branches; + const i = branches.getIndex(byte); + branches.subtrees[i] = res; + const mhpI = might_have_prefix_branches.getIndex(byte); + if (mhpI !== -1) { + might_have_prefix_branches.subtrees[mhpI] = res; + } + } + layer.push(res); + } + } + } + + /** + * All matches for strings that start with the string represented by this node. + * @returns {AsyncGenerator<RoaringBitmap>} + */ + async* prefixMatches() { + /** @type {Promise<SearchTree>[]} */ + let layer = [Promise.resolve(this.tree)]; + while (layer.length) { + const current_layer = layer; + layer = []; + for await (const tree of current_layer) { + yield tree.leaves_whole; + } + /** @type {HashTable<[number, SearchTree][]>} */ + const subnodes = new HashTable(); + for await (const node of current_layer) { + const mhp_branches = node.might_have_prefix_branches; + const l = mhp_branches.subtrees.length; + for (let i = 0; i < l; ++i) { + const subtree = mhp_branches.subtrees[i]; + if (subtree) { + layer.push(subtree); + } else if (subtree === null) { + const byte = mhp_branches.keys[i]; + const newnode = mhp_branches.getHash(i); + if (!newnode) { + throw new Error(`malformed tree; no hash for key ${byte}`); + } else { + let subnode_list = subnodes.get(newnode); + if (!subnode_list) { + subnode_list = [[byte, node]]; + subnodes.set(newnode, subnode_list); + } else { + subnode_list.push([byte, node]); + } + } + } else { + throw new Error(`malformed tree; index ${i} does not exist`); + } + } + } + for (const [newnode, subnode_list] of subnodes.entries()) { + const res = registry.searchTreeLoadByHash(newnode); + for (const [byte, node] of subnode_list) { + const mhp_branches = node.might_have_prefix_branches; + const i = mhp_branches.getIndex(byte); + mhp_branches.subtrees[i] = res; + const branches = node.branches; + const bi = branches.getIndex(byte); + mhp_branches.subtrees[bi] = res; + } + layer.push(res); + } + } + } + + /** + * Returns all keys that are children of this node. + * @returns {Uint8Array} + */ + keys() { + const data = this.tree.data; + if (this.offset === data.length) { + return this.tree.branches.keys; + } else { + return Uint8Array.of(data[this.offset]); + } + } + + /** + * Returns all nodes that are direct children of this node. + * @returns {[number, Promise<Trie>][]} + */ + children() { + const data = this.tree.data; + if (this.offset === data.length) { + /** @type {[number, Promise<Trie>][]} */ + const nodes = []; + let i = 0; + for (const [k, v] of this.tree.branches.entries()) { + /** @type {Promise<SearchTree>} */ + let node; + if (v) { + node = v; + } else { + const newnode = this.tree.branches.getHash(i); + if (!newnode) { + throw new Error(`malformed tree; no hash for key ${k}: ${newnode} \ + ${this.tree.branches.hashes} ${this.tree.branches.keys}`); + } + node = registry.searchTreeLoadByHash(newnode); + this.tree.branches.subtrees[i] = node; + const mhpI = this.tree.might_have_prefix_branches.getIndex(k); + if (mhpI !== -1) { + this.tree.might_have_prefix_branches.subtrees[mhpI] = node; + } + } + nodes.push([k, node.then(node => node.trie())]); + i += 1; + } + return nodes; + } else { + /** @type {number} */ + const codePoint = data[this.offset]; + const trie = new Trie(this.tree, this.offset + 1); + return [[codePoint, Promise.resolve(trie)]]; + } + } + + /** + * Returns all keys that are children of this node. + * @returns {Uint8Array} + */ + keysExcludeSuffixOnly() { + const data = this.tree.data; + if (this.offset === data.length) { + return this.tree.might_have_prefix_branches.keys; + } else { + return Uint8Array.of(data[this.offset]); + } + } + + /** + * Returns all nodes that are direct children of this node. + * @returns {[number, Promise<Trie>][]} + */ + childrenExcludeSuffixOnly() { + const data = this.tree.data; + if (this.offset === data.length) { + /** @type {[number, Promise<Trie>][]} */ + const nodes = []; + let i = 0; + for (const [k, v] of this.tree.might_have_prefix_branches.entries()) { + /** @type {Promise<SearchTree>} */ + let node; + if (v) { + node = v; + } else { + const newnode = this.tree.might_have_prefix_branches.getHash(i); + if (!newnode) { + throw new Error(`malformed tree; no hash for key ${k}`); + } + node = registry.searchTreeLoadByHash(newnode); + this.tree.might_have_prefix_branches.subtrees[i] = node; + this.tree.branches.subtrees[this.tree.branches.getIndex(k)] = node; + } + nodes.push([k, node.then(node => node.trie())]); + i += 1; + } + return nodes; + } else { + /** @type {number} */ + const codePoint = data[this.offset]; + const trie = new Trie(this.tree, this.offset + 1); + return [[codePoint, Promise.resolve(trie)]]; + } + } + + /** + * Returns a single node that is a direct child of this node. + * @param {number} byte + * @returns {Promise<Trie>?} + */ + child(byte) { + if (this.offset === this.tree.data.length) { + const i = this.tree.branches.getIndex(byte); + if (i !== -1) { + let branch = this.tree.branches.subtrees[i]; + if (branch === null) { + const newnode = this.tree.branches.getHash(i); + if (!newnode) { + throw new Error(`malformed tree; no hash for key ${byte}`); + } + branch = registry.searchTreeLoadByHash(newnode); + this.tree.branches.subtrees[i] = branch; + const mhpI = this.tree.might_have_prefix_branches.getIndex(byte); + if (mhpI !== -1) { + this.tree.might_have_prefix_branches.subtrees[mhpI] = branch; + } + } + return branch.then(branch => branch.trie()); + } + } else if (this.tree.data[this.offset] === byte) { + return Promise.resolve(new Trie(this.tree, this.offset + 1)); + } + return null; + } + } + + class DataColumn { + /** + * Construct the wrapper object for a data column. + * @param {number[]} counts + * @param {Uint8Array} hashes + * @param {RoaringBitmap} emptyset + * @param {string} name + */ + constructor(counts, hashes, emptyset, name) { + this.hashes = hashes; + this.emptyset = emptyset; + this.name = name; + /** @type {{"hash": Uint8Array, "data": Promise<Uint8Array[]>?, "end": number}[]} */ + this.buckets = []; + this.bucket_keys = []; + const l = counts.length; + let k = 0; + let totalLength = 0; + for (let i = 0; i < l; ++i) { + const count = counts[i]; + totalLength += count; + const start = k; + for (let j = 0; j < count; ++j) { + if (emptyset.contains(k)) { + j -= 1; + } + k += 1; + } + const end = k; + const bucket = {hash: hashes.subarray(i * 6, (i + 1) * 6), data: null, end, count}; + this.buckets.push(bucket); + this.bucket_keys.push(start); + } + this.length = totalLength; + } + /** + * Check if a cell contains the empty string. + * @param {number} id + * @returns {boolean} + */ + isEmpty(id) { + return this.emptyset.contains(id); + } + /** + * Look up a cell by row ID. + * @param {number} id + * @returns {Promise<Uint8Array|undefined>} + */ + async at(id) { + if (this.emptyset.contains(id)) { + return Promise.resolve(EMPTY_UINT8); + } else { + let idx = -1; + while (this.bucket_keys[idx + 1] <= id) { + idx += 1; + } + if (idx === -1 || idx >= this.bucket_keys.length) { + return Promise.resolve(undefined); + } else { + const start = this.bucket_keys[idx]; + const {hash, end} = this.buckets[idx]; + let data = this.buckets[idx].data; + if (data === null) { + const dataSansEmptyset = await registry.dataLoadByNameAndHash( + this.name, + hash, + ); + const dataWithEmptyset = []; + let pos = start; + let i = 0; + while (pos < end) { + if (this.emptyset.contains(pos)) { + dataWithEmptyset.push(EMPTY_UINT8); + } else { + dataWithEmptyset.push(dataSansEmptyset[i]); + i += 1; + } + pos += 1; + } + data = Promise.resolve(dataWithEmptyset); + this.buckets[idx].data = data; + } + return (await data)[id - start]; + } + } + } + } + + class Database { + /** + * The primary frontend for accessing data in this index. + * + * @param {Map<string, SearchTree>} searchTreeRoots + * @param {Map<string, DataColumn>} dataColumns + */ + constructor(searchTreeRoots, dataColumns) { + this.searchTreeRoots = searchTreeRoots; + this.dataColumns = dataColumns; + } + /** + * Search a column by name, returning verbatim matched IDs. + * @param {string} colname + * @returns {SearchTree|undefined} + */ + getIndex(colname) { + return this.searchTreeRoots.get(colname); + } + /** + * Look up a cell by column ID and row ID. + * @param {string} colname + * @returns {DataColumn|undefined} + */ + getData(colname) { + return this.dataColumns.get(colname); + } + } + + /** + * Load a data column. + * @param {Uint8Array} data + */ + function loadColumnFromBytes(data) { + const hashBuf = Uint8Array.of(0, 0, 0, 0, 0, 0, 0, 0); + const truncatedHash = hashBuf.subarray(2, 8); + siphashOfBytes(data, 0, 0, 0, 0, hashBuf); + const cb = registry.dataColumnLoadPromiseCallbacks.get(truncatedHash); + if (cb) { + const backrefs = []; + const result = []; + let i = 0; + const l = data.length; + while (i < l) { + let c = data[i]; + if (c >= 48 && c <= 63) { // 48 = "0", 63 = "?" + result.push(backrefs[c - 48]); + i += 1; + } else { + let n = 0; + while (c < 96) { // 96 = "`" + n = (n << 4) | (c & 0xF); + i += 1; + c = data[i]; + } + n = (n << 4) | (c & 0xF); + i += 1; + const item = data.slice(i, i + n); + result.push(item); + i += n; + backrefs.unshift(item); + if (backrefs.length > 16) { + backrefs.pop(); + } + } + } + cb(null, result); + } + } + + /** + * @param {string} inputBase64 + * @returns {[Uint8Array, SearchTree]} + */ + function makeSearchTreeFromBase64(inputBase64) { + const input = makeUint8ArrayFromBase64(inputBase64); + let i = 0; + const l = input.length; + /** @type {HashTable<SearchTree>} */ + const stash = new HashTable(); + const hash = Uint8Array.of(0, 0, 0, 0, 0, 0, 0, 0); + const truncatedHash = new Uint8Array(hash.buffer, 2, 6); + // used for handling compressed (that is, relative-offset) nodes + /** @type {{hash: Uint8Array, used: boolean}[]} */ + const hash_history = []; + /** @type {Uint8Array[]} */ + const data_history = []; + let canonical = EMPTY_UINT8; + /** @type {SearchTree} */ + let tree = new SearchTree( + EMPTY_UINT8, + EMPTY_UINT8, + EMPTY_UINT8, + EMPTY_UINT8, + EMPTY_UINT8, + EMPTY_BITMAP, + EMPTY_BITMAP, + ); + while (i < l) { + const start = i; + let data; + if (input[i] !== 0) { + // compressed node + const compression_tag = input[i]; + const all_children_are_compressed = (compression_tag & 0x3F) === 0x3F; + const any_children_are_compressed = (compression_tag & 0x3F) !== 0x00; + const is_data_compressed = (compression_tag & 0x80) !== 0; + const is_stack_compressed = (compression_tag & 0x40) !== 0; + i += 1; + let dlen = input[i]; + i += 1; + if (is_data_compressed) { + data = data_history[data_history.length - dlen - 1]; + dlen = data.length; + } else { + data = new Uint8Array(input.buffer, i + input.byteOffset, dlen); + i += dlen; + } + const cplen_ = input[i]; + let cplen = input[i]; + i += 1; + const cslen_ = input[i]; + let cslen = input[i]; + i += 1; + if (cplen === 0xff && cslen === 0xff) { + cplen = 0x100; + cslen = 0; + } + let j = 0; + const cpoff = i; + if (any_children_are_compressed) { + while (j < cplen) { + i += all_children_are_compressed || + ((0x80 >> (j + 2)) & compression_tag) !== 0 ? + (is_stack_compressed ? 0 : 1) : + 6; + j += 1; + } + } else { + i += cplen * 6; + } + j = 0; + const csoff = i; + if (any_children_are_compressed) { + while (j < cslen) { + i += all_children_are_compressed || + ((0x80 >> (cplen + j + 2)) & compression_tag) !== 0 ? + (is_stack_compressed ? 0 : 1) : + 6; + j += 1; + } + } else { + i += cslen * 6; + } + const cpbranches = new Uint8Array(input.buffer, i + input.byteOffset, cplen); + i += cplen; + const csbranches = new Uint8Array(input.buffer, i + input.byteOffset, cslen); + i += cslen; + let whole; + let suffix; + if (input[i] === 0xff) { + whole = EMPTY_BITMAP; + suffix = EMPTY_BITMAP1; + i += 1; + } else { + whole = input[i] === 0 ? + EMPTY_BITMAP1 : + new RoaringBitmap(new Uint8Array(input.buffer, i + input.byteOffset)); + i += whole.consumed_len_bytes; + suffix = input[i] === 0 ? + EMPTY_BITMAP1 : + new RoaringBitmap(new Uint8Array(input.buffer, i + input.byteOffset)); + i += suffix.consumed_len_bytes; + } + /** @type {Uint8Array} */ + let cphashes; + j = 0; + let off_i = 0; + if (any_children_are_compressed) { + cphashes = new Uint8Array(cplen * 6); + while (j < cplen) { + const is_compressed = all_children_are_compressed || + ((0x80 >> (j + 2)) & compression_tag) !== 0; + if (is_compressed) { + let slot = hash_history.length - 1; + if (is_stack_compressed) { + while (hash_history[slot].used) { + slot -= 1; + } + } else { + slot -= input[cpoff + off_i]; + off_i += 1; + } + hash_history[slot].used = true; + cphashes.set( + hash_history[slot].hash, + j * 6, + ); + } else { + cphashes.set( + input.subarray(cpoff + off_i, cpoff + off_i + 6), + j * 6, + ); + off_i += 6; + } + j += 1; + } + } else { + cphashes = input.subarray(cpoff, cpoff + (cplen * 6)); + } + /** @type {Uint8Array} */ + let cshashes; + j = 0; + off_i = 0; + if (any_children_are_compressed) { + cshashes = new Uint8Array(cslen * 6); + while (j < cslen) { + const is_compressed = all_children_are_compressed || + ((0x80 >> (cplen + j + 2)) & compression_tag) !== 0; + if (is_compressed) { + let slot = hash_history.length - 1; + if (is_stack_compressed) { + while (hash_history[slot].used) { + slot -= 1; + } + } else { + slot -= input[csoff + off_i]; + off_i += 1; + } + hash_history[slot].used = true; + cshashes.set( + hash_history[slot].hash, + j * 6, + ); + } else { + cshashes.set( + input.subarray(csoff + off_i, csoff + off_i + 6), + j * 6, + ); + off_i += 6; + } + j += 1; + } + } else { + cshashes = input.subarray(csoff, csoff + (cslen * 6)); + } + tree = new SearchTree( + cpbranches, + cphashes, + csbranches, + cshashes, + data, + whole, + suffix, + ); + const clen = ( + 4 + // lengths of children and data + dlen + + ((cplen + cslen) * 6) + + (cplen + cslen) + + whole.consumed_len_bytes + + suffix.consumed_len_bytes + ); + if (canonical.length < clen) { + canonical = new Uint8Array(clen); + } + let ci = 0; + canonical[ci] = 0; + ci += 1; + canonical[ci] = dlen; + ci += 1; + canonical.set(data, ci); + ci += dlen; + canonical[ci] = cplen_; + ci += 1; + canonical[ci] = cslen_; + ci += 1; + canonical.set(cphashes, ci); + ci += cplen * 6; + canonical.set(cshashes, ci); + ci += cslen * 6; + canonical.set(cpbranches, ci); + ci += cplen; + canonical.set(csbranches, ci); + ci += cslen; + canonical.set(input.subarray( + i - whole.consumed_len_bytes - suffix.consumed_len_bytes, + i, + ), ci); + siphashOfBytes(canonical.subarray(0, clen), 0, 0, 0, 0, hash); + hash[2] &= 0x7f; + } else { + // uncompressed node + const dlen = input [i + 1]; + i += 2; + if (dlen === 0) { + data = EMPTY_UINT8; + } else { + data = new Uint8Array(input.buffer, i + input.byteOffset, dlen); + } + i += dlen; + let cplen = input[i]; + i += 1; + let cslen = input[i]; + i += 1; + if (cplen === 0xff && cslen === 0xff) { + cplen = 0x100; + cslen = 0; + } + const cphashes = new Uint8Array(input.buffer, i + input.byteOffset, cplen * 6); + i += cplen * 6; + const cshashes = new Uint8Array(input.buffer, i + input.byteOffset, cslen * 6); + i += cslen * 6; + const cpbranches = new Uint8Array(input.buffer, i + input.byteOffset, cplen); + i += cplen; + const csbranches = new Uint8Array(input.buffer, i + input.byteOffset, cslen); + i += cslen; + let whole; + let suffix; + if (input[i] === 0xff) { + whole = EMPTY_BITMAP; + suffix = EMPTY_BITMAP; + i += 1; + } else { + whole = input[i] === 0 ? + EMPTY_BITMAP1 : + new RoaringBitmap(new Uint8Array(input.buffer, i + input.byteOffset)); + i += whole.consumed_len_bytes; + suffix = input[i] === 0 ? + EMPTY_BITMAP1 : + new RoaringBitmap(new Uint8Array(input.buffer, i + input.byteOffset)); + i += suffix.consumed_len_bytes; + } + siphashOfBytes(new Uint8Array( + input.buffer, + start + input.byteOffset, + i - start, + ), 0, 0, 0, 0, hash); + hash[2] &= 0x7f; + tree = new SearchTree( + cpbranches, + cphashes, + csbranches, + cshashes, + data, + whole, + suffix, + ); + } + hash_history.push({hash: truncatedHash.slice(), used: false}); + if (data.length !== 0) { + data_history.push(data); + } + const tree_branch_hashes = tree.branches.hashes; + const tree_branch_subtrees = tree.branches.subtrees; + let j = 0; + let lb = tree.branches.keys.length; + while (j < lb) { + const subtree = stash.get(tree_branch_hashes.subarray(j * 6, (j + 1) * 6)); + if (subtree !== undefined) { + tree_branch_subtrees[j] = Promise.resolve(subtree); + } + j += 1; + } + const tree_mhp_branch_hashes = tree.might_have_prefix_branches.hashes; + const tree_mhp_branch_subtrees = tree.might_have_prefix_branches.subtrees; + j = 0; + lb = tree.might_have_prefix_branches.keys.length; + while (j < lb) { + const subtree = stash.get(tree_mhp_branch_hashes.subarray(j * 6, (j + 1) * 6)); + if (subtree !== undefined) { + tree_mhp_branch_subtrees[j] = Promise.resolve(subtree); + } + j += 1; + } + if (i !== l) { + stash.set(truncatedHash, tree); + } + } + return [truncatedHash, tree]; + } + + return new Promise((resolve, reject) => { + registry.searchTreeRootCallback = (error, data) => { + if (data) { + resolve(data); + } else { + reject(error); + } + }; + hooks.loadRoot(callbacks); + }); +} + +if (typeof window !== "undefined") { + window.Stringdex = { + loadDatabase, + }; + window.RoaringBitmap = RoaringBitmap; + if (window.StringdexOnload) { + window.StringdexOnload.forEach(cb => cb(window.Stringdex)); + } +} else { + /** @type {stringdex.Stringdex} */ + // eslint-disable-next-line no-undef + module.exports.Stringdex = { + loadDatabase, + }; + /** @type {stringdex.RoaringBitmap} */ + // eslint-disable-next-line no-undef + module.exports.RoaringBitmap = RoaringBitmap; +} + +// eslint-disable-next-line max-len +// polyfill https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array/fromBase64 +/** + * @type {function(string): Uint8Array} base64 + */ +//@ts-expect-error +const makeUint8ArrayFromBase64 = Uint8Array.fromBase64 ? Uint8Array.fromBase64 : (string => { + const bytes_as_string = atob(string); + const l = bytes_as_string.length; + const bytes = new Uint8Array(l); + for (let i = 0; i < l; ++i) { + bytes[i] = bytes_as_string.charCodeAt(i); + } + return bytes; +}); +/** + * @type {function(string): Uint8Array} base64 + */ +//@ts-expect-error +const makeUint8ArrayFromHex = Uint8Array.fromHex ? Uint8Array.fromHex : (string => { + /** @type {Object<string, number>} */ + const alpha = { + "0": 0, "1": 1, + "2": 2, "3": 3, + "4": 4, "5": 5, + "6": 6, "7": 7, + "8": 8, "9": 9, + "a": 10, "b": 11, + "A": 10, "B": 11, + "c": 12, "d": 13, + "C": 12, "D": 13, + "e": 14, "f": 15, + "E": 14, "F": 15, + }; + const l = string.length >> 1; + const bytes = new Uint8Array(l); + for (let i = 0; i < l; i += 1) { + const top = string[i << 1]; + const bottom = string[(i << 1) + 1]; + bytes[i] = (alpha[top] << 4) | alpha[bottom]; + } + return bytes; +}); + +/** + * @type {function(Uint8Array): string} base64 + */ +//@ts-expect-error +const makeHexFromUint8Array = Uint8Array.prototype.toHex ? (array => array.toHex()) : (array => { + /** @type {string[]} */ + const alpha = [ + "0", "1", + "2", "3", + "4", "5", + "6", "7", + "8", "9", + "a", "b", + "c", "d", + "e", "f", + ]; + const l = array.length; + const v = []; + for (let i = 0; i < l; ++i) { + v.push(alpha[array[i] >> 4]); + v.push(alpha[array[i] & 0xf]); + } + return v.join(""); +}); + +////////////// + +/** + * SipHash 1-3 + * @param {Uint8Array} input data to be hashed; all codepoints in string should be less than 256 + * @param {number} k0lo first word of key + * @param {number} k0hi second word of key + * @param {number} k1lo third word of key + * @param {number} k1hi fourth word of key + * @param {Uint8Array} output the data to write (clobber the first eight bytes) + */ +function siphashOfBytes(input, k0lo, k0hi, k1lo, k1hi, output) { + // hash state + // While siphash uses 64 bit state, js only has native support + // for 32 bit numbers. BigInt, unfortunately, doesn't count. + // It's too slow. + let v0lo = k0lo ^ 0x70736575; + let v0hi = k0hi ^ 0x736f6d65; + let v1lo = k1lo ^ 0x6e646f6d; + let v1hi = k1hi ^ 0x646f7261; + let v2lo = k0lo ^ 0x6e657261; + let v2hi = k0hi ^ 0x6c796765; + let v3lo = k1lo ^ 0x79746573; + let v3hi = k1hi ^ 0x74656462; + const inputLength = input.length; + let inputI = 0; + // main hash loop + const left = inputLength & 0x7; + let milo = 0; + let mihi = 0; + while (inputI < inputLength - left) { + u8ToU64le(inputI, inputI + 8); + v3lo ^= milo; + v3hi ^= mihi; + siphashCompress(); + v0lo ^= milo; + v0hi ^= mihi; + inputI += 8; + } + u8ToU64le(inputI, inputI + left); + // finish + const blo = milo; + const bhi = ((inputLength & 0xff) << 24) | mihi; + v3lo ^= blo; + v3hi ^= bhi; + siphashCompress(); + v0lo ^= blo; + v0hi ^= bhi; + v2lo ^= 0xff; + siphashCompress(); + siphashCompress(); + siphashCompress(); + output[7] = (v0lo ^ v1lo ^ v2lo ^ v3lo) & 0xff; + output[6] = (v0lo ^ v1lo ^ v2lo ^ v3lo) >>> 8; + output[5] = (v0lo ^ v1lo ^ v2lo ^ v3lo) >>> 16; + output[4] = (v0lo ^ v1lo ^ v2lo ^ v3lo) >>> 24; + output[3] = (v0hi ^ v1hi ^ v2hi ^ v3hi) & 0xff; + output[2] = (v0hi ^ v1hi ^ v2hi ^ v3hi) >>> 8; + output[1] = (v0hi ^ v1hi ^ v2hi ^ v3hi) >>> 16; + output[0] = (v0hi ^ v1hi ^ v2hi ^ v3hi) >>> 24; + /** + * Convert eight bytes to a single 64-bit number + * @param {number} offset + * @param {number} length + */ + function u8ToU64le(offset, length) { + const n0 = offset < length ? input[offset] & 0xff : 0; + const n1 = offset + 1 < length ? input[offset + 1] & 0xff : 0; + const n2 = offset + 2 < length ? input[offset + 2] & 0xff : 0; + const n3 = offset + 3 < length ? input[offset + 3] & 0xff : 0; + const n4 = offset + 4 < length ? input[offset + 4] & 0xff : 0; + const n5 = offset + 5 < length ? input[offset + 5] & 0xff : 0; + const n6 = offset + 6 < length ? input[offset + 6] & 0xff : 0; + const n7 = offset + 7 < length ? input[offset + 7] & 0xff : 0; + milo = n0 | (n1 << 8) | (n2 << 16) | (n3 << 24); + mihi = n4 | (n5 << 8) | (n6 << 16) | (n7 << 24); + } + function siphashCompress() { + // v0 += v1; + v0hi = (v0hi + v1hi + (((v0lo >>> 0) + (v1lo >>> 0) > 0xffffffff) ? 1 : 0)) | 0; + v0lo = (v0lo + v1lo) | 0; + // rotl(v1, 13) + let v1lo_ = v1lo; + let v1hi_ = v1hi; + v1lo = (v1lo_ << 13) | (v1hi_ >>> 19); + v1hi = (v1hi_ << 13) | (v1lo_ >>> 19); + // v1 ^= v0 + v1lo ^= v0lo; + v1hi ^= v0hi; + // rotl(v0, 32) + const v0lo_ = v0lo; + const v0hi_ = v0hi; + v0lo = v0hi_; + v0hi = v0lo_; + // v2 += v3 + v2hi = (v2hi + v3hi + (((v2lo >>> 0) + (v3lo >>> 0) > 0xffffffff) ? 1 : 0)) | 0; + v2lo = (v2lo + v3lo) | 0; + // rotl(v3, 16) + let v3lo_ = v3lo; + let v3hi_ = v3hi; + v3lo = (v3lo_ << 16) | (v3hi_ >>> 16); + v3hi = (v3hi_ << 16) | (v3lo_ >>> 16); + // v3 ^= v2 + v3lo ^= v2lo; + v3hi ^= v2hi; + // v0 += v3 + v0hi = (v0hi + v3hi + (((v0lo >>> 0) + (v3lo >>> 0) > 0xffffffff) ? 1 : 0)) | 0; + v0lo = (v0lo + v3lo) | 0; + // rotl(v3, 21) + v3lo_ = v3lo; + v3hi_ = v3hi; + v3lo = (v3lo_ << 21) | (v3hi_ >>> 11); + v3hi = (v3hi_ << 21) | (v3lo_ >>> 11); + // v3 ^= v0 + v3lo ^= v0lo; + v3hi ^= v0hi; + // v2 += v1 + v2hi = (v2hi + v1hi + (((v2lo >>> 0) + (v1lo >>> 0) > 0xffffffff) ? 1 : 0)) | 0; + v2lo = (v2lo + v1lo) | 0; + // rotl(v1, 17) + v1lo_ = v1lo; + v1hi_ = v1hi; + v1lo = (v1lo_ << 17) | (v1hi_ >>> 15); + v1hi = (v1hi_ << 17) | (v1lo_ >>> 15); + // v1 ^= v2 + v1lo ^= v2lo; + v1hi ^= v2hi; + // rotl(v2, 32) + const v2lo_ = v2lo; + const v2hi_ = v2hi; + v2lo = v2hi_; + v2hi = v2lo_; + } +} + +////////////// + + +// Parts of this code are based on Lucene, which is licensed under the +// Apache/2.0 license. +// More information found here: +// https://fossies.org/linux/lucene/lucene/core/src/java/org/apache/lucene/util/automaton/ +// LevenshteinAutomata.java +class ParametricDescription { + /** + * @param {number} w + * @param {number} n + * @param {Int32Array} minErrors + */ + constructor(w, n, minErrors) { + this.w = w; + this.n = n; + this.minErrors = minErrors; + } + /** + * @param {number} absState + * @returns {boolean} + */ + isAccept(absState) { + const state = Math.floor(absState / (this.w + 1)); + const offset = absState % (this.w + 1); + return this.w - offset + this.minErrors[state] <= this.n; + } + /** + * @param {number} absState + * @returns {number} + */ + getPosition(absState) { + return absState % (this.w + 1); + } + /** + * @param {Uint8Array} name + * @param {number} charCode + * @param {number} pos + * @param {number} end + * @returns {number} + */ + getVector(name, charCode, pos, end) { + let vector = 0; + for (let i = pos; i < end; i += 1) { + vector = vector << 1; + if (name[i] === charCode) { + vector |= 1; + } + } + return vector; + } + /** + * @param {Int32Array} data + * @param {number} index + * @param {number} bitsPerValue + * @returns {number} + */ + unpack(data, index, bitsPerValue) { + const bitLoc = (bitsPerValue * index); + const dataLoc = bitLoc >> 5; + const bitStart = bitLoc & 31; + if (bitStart + bitsPerValue <= 32) { + // not split + return ((data[dataLoc] >> bitStart) & this.MASKS[bitsPerValue - 1]); + } else { + // split + const part = 32 - bitStart; + return ~~(((data[dataLoc] >> bitStart) & this.MASKS[part - 1]) + + ((data[1 + dataLoc] & this.MASKS[bitsPerValue - part - 1]) << part)); + } + } +} +ParametricDescription.prototype.MASKS = new Int32Array([ + 0x1, 0x3, 0x7, 0xF, + 0x1F, 0x3F, 0x7F, 0xFF, + 0x1FF, 0x3F, 0x7FF, 0xFFF, + 0x1FFF, 0x3FFF, 0x7FFF, 0xFFFF, + 0x1FFFF, 0x3FFFF, 0x7FFFF, 0xFFFFF, + 0x1FFFFF, 0x3FFFFF, 0x7FFFFF, 0xFFFFFF, + 0x1FFFFFF, 0x3FFFFFF, 0x7FFFFFF, 0xFFFFFFF, + 0x1FFFFFFF, 0x3FFFFFFF, 0x7FFFFFFF, 0xFFFFFFFF, +]); + +// The following code was generated with the moman/finenight pkg +// This package is available under the MIT License, see NOTICE.txt +// for more details. +// This class is auto-generated, Please do not modify it directly. +// You should modify the https://gitlab.com/notriddle/createAutomata.py instead. +// The following code was generated with the moman/finenight pkg +// This package is available under the MIT License, see NOTICE.txt +// for more details. +// This class is auto-generated, Please do not modify it directly. +// You should modify https://gitlab.com/notriddle/moman-rustdoc instead. + +class Lev2TParametricDescription extends ParametricDescription { + /** + * @param {number} absState + * @param {number} position + * @param {number} vector + * @returns {number} + */ + transition(absState, position, vector) { + let state = Math.floor(absState / (this.w + 1)); + let offset = absState % (this.w + 1); + + if (position === this.w) { + if (state < 3) { + const loc = Math.imul(vector, 3) + state; + offset += this.unpack(this.offsetIncrs0, loc, 1); + state = this.unpack(this.toStates0, loc, 2) - 1; + } + } else if (position === this.w - 1) { + if (state < 5) { + const loc = Math.imul(vector, 5) + state; + offset += this.unpack(this.offsetIncrs1, loc, 1); + state = this.unpack(this.toStates1, loc, 3) - 1; + } + } else if (position === this.w - 2) { + if (state < 13) { + const loc = Math.imul(vector, 13) + state; + offset += this.unpack(this.offsetIncrs2, loc, 2); + state = this.unpack(this.toStates2, loc, 4) - 1; + } + } else if (position === this.w - 3) { + if (state < 28) { + const loc = Math.imul(vector, 28) + state; + offset += this.unpack(this.offsetIncrs3, loc, 2); + state = this.unpack(this.toStates3, loc, 5) - 1; + } + } else if (position === this.w - 4) { + if (state < 45) { + const loc = Math.imul(vector, 45) + state; + offset += this.unpack(this.offsetIncrs4, loc, 3); + state = this.unpack(this.toStates4, loc, 6) - 1; + } + } else { + // eslint-disable-next-line no-lonely-if + if (state < 45) { + const loc = Math.imul(vector, 45) + state; + offset += this.unpack(this.offsetIncrs5, loc, 3); + state = this.unpack(this.toStates5, loc, 6) - 1; + } + } + + if (state === -1) { + // null state + return -1; + } else { + // translate back to abs + return Math.imul(state, this.w + 1) + offset; + } + } + + // state map + // 0 -> [(0, 0)] + // 1 -> [(0, 1)] + // 2 -> [(0, 2)] + // 3 -> [(0, 1), (1, 1)] + // 4 -> [(0, 2), (1, 2)] + // 5 -> [(0, 1), (1, 1), (2, 1)] + // 6 -> [(0, 2), (1, 2), (2, 2)] + // 7 -> [(0, 1), (2, 1)] + // 8 -> [(0, 1), (2, 2)] + // 9 -> [(0, 2), (2, 1)] + // 10 -> [(0, 2), (2, 2)] + // 11 -> [t(0, 1), (0, 1), (1, 1), (2, 1)] + // 12 -> [t(0, 2), (0, 2), (1, 2), (2, 2)] + // 13 -> [(0, 2), (1, 2), (2, 2), (3, 2)] + // 14 -> [(0, 1), (1, 1), (3, 2)] + // 15 -> [(0, 1), (2, 2), (3, 2)] + // 16 -> [(0, 1), (3, 2)] + // 17 -> [(0, 1), t(1, 2), (2, 2), (3, 2)] + // 18 -> [(0, 2), (1, 2), (3, 1)] + // 19 -> [(0, 2), (1, 2), (3, 2)] + // 20 -> [(0, 2), (1, 2), t(1, 2), (2, 2), (3, 2)] + // 21 -> [(0, 2), (2, 1), (3, 1)] + // 22 -> [(0, 2), (2, 2), (3, 2)] + // 23 -> [(0, 2), (3, 1)] + // 24 -> [(0, 2), (3, 2)] + // 25 -> [(0, 2), t(1, 2), (1, 2), (2, 2), (3, 2)] + // 26 -> [t(0, 2), (0, 2), (1, 2), (2, 2), (3, 2)] + // 27 -> [t(0, 2), (0, 2), (1, 2), (3, 1)] + // 28 -> [(0, 2), (1, 2), (2, 2), (3, 2), (4, 2)] + // 29 -> [(0, 2), (1, 2), (2, 2), (4, 2)] + // 30 -> [(0, 2), (1, 2), (2, 2), t(2, 2), (3, 2), (4, 2)] + // 31 -> [(0, 2), (1, 2), (3, 2), (4, 2)] + // 32 -> [(0, 2), (1, 2), (4, 2)] + // 33 -> [(0, 2), (1, 2), t(1, 2), (2, 2), (3, 2), (4, 2)] + // 34 -> [(0, 2), (1, 2), t(2, 2), (2, 2), (3, 2), (4, 2)] + // 35 -> [(0, 2), (2, 1), (4, 2)] + // 36 -> [(0, 2), (2, 2), (3, 2), (4, 2)] + // 37 -> [(0, 2), (2, 2), (4, 2)] + // 38 -> [(0, 2), (3, 2), (4, 2)] + // 39 -> [(0, 2), (4, 2)] + // 40 -> [(0, 2), t(1, 2), (1, 2), (2, 2), (3, 2), (4, 2)] + // 41 -> [(0, 2), t(2, 2), (2, 2), (3, 2), (4, 2)] + // 42 -> [t(0, 2), (0, 2), (1, 2), (2, 2), (3, 2), (4, 2)] + // 43 -> [t(0, 2), (0, 2), (1, 2), (2, 2), (4, 2)] + // 44 -> [t(0, 2), (0, 2), (1, 2), (2, 2), t(2, 2), (3, 2), (4, 2)] + + + /** @param {number} w - length of word being checked */ + constructor(w) { + super(w, 2, new Int32Array([ + 0,1,2,0,1,-1,0,-1,0,-1,0,-1,0,-1,-1,-1,-1,-1,-2,-1,-1,-2,-1,-2, + -1,-1,-1,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2, + ])); + } +} + +Lev2TParametricDescription.prototype.toStates0 = /*2 bits per value */ new Int32Array([ + 0xe, +]); +Lev2TParametricDescription.prototype.offsetIncrs0 = /*1 bits per value */ new Int32Array([ + 0x0, +]); + +Lev2TParametricDescription.prototype.toStates1 = /*3 bits per value */ new Int32Array([ + 0x1a688a2c, +]); +Lev2TParametricDescription.prototype.offsetIncrs1 = /*1 bits per value */ new Int32Array([ + 0x3e0, +]); + +Lev2TParametricDescription.prototype.toStates2 = /*4 bits per value */ new Int32Array([ + 0x70707054,0xdc07035,0x3dd3a3a,0x2323213a, + 0x15435223,0x22545432,0x5435, +]); +Lev2TParametricDescription.prototype.offsetIncrs2 = /*2 bits per value */ new Int32Array([ + 0x80000,0x55582088,0x55555555,0x55, +]); + +Lev2TParametricDescription.prototype.toStates3 = /*5 bits per value */ new Int32Array([ + 0x1c0380a4,0x700a570,0xca529c0,0x180a00, + 0xa80af180,0xc5498e60,0x5a546398,0x8c4300e8, + 0xac18c601,0xd8d43501,0x863500ad,0x51976d6a, + 0x8ca0180a,0xc3501ac2,0xb0c5be16,0x76dda8a5, + 0x18c4519,0xc41294a,0xe248d231,0x1086520c, + 0xce31ac42,0x13946358,0x2d0348c4,0x6732d494, + 0x1ad224a5,0xd635ad4b,0x520c4139,0xce24948, + 0x22110a52,0x58ce729d,0xc41394e3,0x941cc520, + 0x90e732d4,0x4729d224,0x39ce35ad, +]); +Lev2TParametricDescription.prototype.offsetIncrs3 = /*2 bits per value */ new Int32Array([ + 0x80000,0xc0c830,0x300f3c30,0x2200fcff, + 0xcaa00a08,0x3c2200a8,0xa8fea00a,0x55555555, + 0x55555555,0x55555555,0x55555555,0x55555555, + 0x55555555,0x55555555, +]); + +Lev2TParametricDescription.prototype.toStates4 = /*6 bits per value */ new Int32Array([ + 0x801c0144,0x1453803,0x14700038,0xc0005145, + 0x1401,0x14,0x140000,0x0, + 0x510000,0x6301f007,0x301f00d1,0xa186178, + 0xc20ca0c3,0xc20c30,0xc30030c,0xc00c00cd, + 0xf0c00c30,0x4c054014,0xc30944c3,0x55150c34, + 0x8300550,0x430c0143,0x50c31,0xc30850c, + 0xc3143000,0x50053c50,0x5130d301,0x850d30c2, + 0x30a08608,0xc214414,0x43142145,0x21450031, + 0x1400c314,0x4c143145,0x32832803,0x28014d6c, + 0xcd34a0c3,0x1c50c76,0x1c314014,0x430c30c3, + 0x1431,0xc300500,0xca00d303,0xd36d0e40, + 0x90b0e400,0xcb2abb2c,0x70c20ca1,0x2c32ca2c, + 0xcd2c70cb,0x31c00c00,0x34c2c32c,0x5583280, + 0x558309b7,0x6cd6ca14,0x430850c7,0x51c51401, + 0x1430c714,0xc3087,0x71451450,0xca00d30, + 0xc26dc156,0xb9071560,0x1cb2abb2,0xc70c2144, + 0xb1c51ca1,0x1421c70c,0xc51c00c3,0x30811c51, + 0x24324308,0xc51031c2,0x70820820,0x5c33830d, + 0xc33850c3,0x30c30c30,0xc30c31c,0x451450c3, + 0x20c20c20,0xda0920d,0x5145914f,0x36596114, + 0x51965865,0xd9643653,0x365a6590,0x51964364, + 0x43081505,0x920b2032,0x2c718b28,0xd7242249, + 0x35cb28b0,0x2cb3872c,0x972c30d7,0xb0c32cb2, + 0x4e1c75c,0xc80c90c2,0x62ca2482,0x4504171c, + 0xd65d9610,0x33976585,0xd95cb5d,0x4b5ca5d7, + 0x73975c36,0x10308138,0xc2245105,0x41451031, + 0x14e24208,0xc35c3387,0x51453851,0x1c51c514, + 0xc70c30c3,0x20451450,0x14f1440c,0x4f0da092, + 0x4513d41,0x6533944d,0x1350e658,0xe1545055, + 0x64365a50,0x5519383,0x51030815,0x28920718, + 0x441c718b,0x714e2422,0x1c35cb28,0x4e1c7387, + 0xb28e1c51,0x5c70c32c,0xc204e1c7,0x81c61440, + 0x1c62ca24,0xd04503ce,0x85d63944,0x39338e65, + 0x8e154387,0x364b5ca3,0x38739738, +]); +Lev2TParametricDescription.prototype.offsetIncrs4 = /*3 bits per value */ new Int32Array([ + 0x10000000,0xc00000,0x60061,0x400, + 0x0,0x80010008,0x249248a4,0x8229048, + 0x2092,0x6c3603,0xb61b6c30,0x6db6036d, + 0xdb6c0,0x361b0180,0x91b72000,0xdb11b71b, + 0x6db6236,0x1008200,0x12480012,0x24924906, + 0x48200049,0x80410002,0x24000900,0x4924a489, + 0x10822492,0x20800125,0x48360,0x9241b692, + 0x6da4924,0x40009268,0x241b010,0x291b4900, + 0x6d249249,0x49493423,0x92492492,0x24924924, + 0x49249249,0x92492492,0x24924924,0x49249249, + 0x92492492,0x24924924,0x49249249,0x92492492, + 0x24924924,0x49249249,0x92492492,0x24924924, + 0x49249249,0x92492492,0x24924924,0x49249249, + 0x92492492,0x24924924,0x49249249,0x92492492, + 0x24924924,0x49249249,0x92492492,0x24924924, + 0x49249249,0x92492492,0x24924924,0x49249249, + 0x92492492,0x24924924,0x49249249,0x2492, +]); + +Lev2TParametricDescription.prototype.toStates5 = /*6 bits per value */ new Int32Array([ + 0x801c0144,0x1453803,0x14700038,0xc0005145, + 0x1401,0x14,0x140000,0x0, + 0x510000,0x4e00e007,0xe0051,0x3451451c, + 0xd015000,0x30cd0000,0xc30c30c,0xc30c30d4, + 0x40c30c30,0x7c01c014,0xc03458c0,0x185e0c07, + 0x2830c286,0x830c3083,0xc30030,0x33430c, + 0x30c3003,0x70051030,0x16301f00,0x8301f00d, + 0x30a18617,0xc20ca0c,0x431420c3,0xb1450c51, + 0x14314315,0x4f143145,0x34c05401,0x4c30944c, + 0x55150c3,0x30830055,0x1430c014,0xc00050c3, + 0xc30850,0xc314300,0x150053c5,0x25130d30, + 0x5430d30c,0xc0354154,0x300d0c90,0x1cb2cd0c, + 0xc91cb0c3,0x72c30cb2,0x14f1cb2c,0xc34c0540, + 0x34c30944,0x82182214,0x851050c2,0x50851430, + 0x1400c50c,0x30c5085,0x50c51450,0x150053c, + 0xc25130d3,0x8850d30,0x1430a086,0x450c2144, + 0x51cb1c21,0x1c91c70c,0xc71c314b,0x34c1cb1, + 0x6c328328,0xc328014d,0x76cd34a0,0x1401c50c, + 0xc31c3140,0x31430c30,0x14,0x30c3005, + 0xa0ca00d3,0x535b0c,0x4d2830ca,0x514369b3, + 0xc500d01,0x5965965a,0x30d46546,0x6435030c, + 0x8034c659,0xdb439032,0x2c390034,0xcaaecb24, + 0x30832872,0xcb28b1c,0x4b1c32cb,0x70030033, + 0x30b0cb0c,0xe40ca00d,0x400d36d0,0xb2c90b0e, + 0xca1cb2ab,0xa2c70c20,0x6575d95c,0x4315b5ce, + 0x95c53831,0x28034c5d,0x9b705583,0xa1455830, + 0xc76cd6c,0x40143085,0x71451c51,0x871430c, + 0x450000c3,0xd3071451,0x1560ca00,0x560c26dc, + 0xb35b2851,0xc914369,0x1a14500d,0x46593945, + 0xcb2c939,0x94507503,0x328034c3,0x9b70558, + 0xe41c5583,0x72caaeca,0x1c308510,0xc7147287, + 0x50871c32,0x1470030c,0xd307147,0xc1560ca0, + 0x1560c26d,0xabb2b907,0x21441cb2,0x38a1c70c, + 0x8e657394,0x314b1c93,0x39438738,0x43083081, + 0x31c22432,0x820c510,0x830d7082,0x50c35c33, + 0xc30c338,0xc31c30c3,0x50c30c30,0xc204514, + 0x890c90c2,0x31440c70,0xa8208208,0xea0df0c3, + 0x8a231430,0xa28a28a2,0x28a28a1e,0x1861868a, + 0x48308308,0xc3682483,0x14516453,0x4d965845, + 0xd4659619,0x36590d94,0xd969964,0x546590d9, + 0x20c20541,0x920d20c,0x5914f0da,0x96114514, + 0x65865365,0xe89d3519,0x99e7a279,0x9e89e89e, + 0x81821827,0xb2032430,0x18b28920,0x422492c7, + 0xb28b0d72,0x3872c35c,0xc30d72cb,0x32cb2972, + 0x1c75cb0c,0xc90c204e,0xa2482c80,0x24b1c62c, + 0xc3a89089,0xb0ea2e42,0x9669a31c,0xa4966a28, + 0x59a8a269,0x8175e7a,0xb203243,0x718b2892, + 0x4114105c,0x17597658,0x74ce5d96,0x5c36572d, + 0xd92d7297,0xe1ce5d70,0xc90c204,0xca2482c8, + 0x4171c62,0x5d961045,0x976585d6,0x79669533, + 0x964965a2,0x659689e6,0x308175e7,0x24510510, + 0x451031c2,0xe2420841,0x5c338714,0x453851c3, + 0x51c51451,0xc30c31c,0x451450c7,0x41440c20, + 0xc708914,0x82105144,0xf1c58c90,0x1470ea0d, + 0x61861863,0x8a1e85e8,0x8687a8a2,0x3081861, + 0x24853c51,0x5053c368,0x1341144f,0x96194ce5, + 0x1544d439,0x94385514,0xe0d90d96,0x5415464, + 0x4f1440c2,0xf0da0921,0x4513d414,0x533944d0, + 0x350e6586,0x86082181,0xe89e981d,0x18277689, + 0x10308182,0x89207185,0x41c718b2,0x14e24224, + 0xc35cb287,0xe1c73871,0x28e1c514,0xc70c32cb, + 0x204e1c75,0x1c61440c,0xc62ca248,0x90891071, + 0x2e41c58c,0xa31c70ea,0xe86175e7,0xa269a475, + 0x5e7a57a8,0x51030817,0x28920718,0xf38718b, + 0xe5134114,0x39961758,0xe1ce4ce,0x728e3855, + 0x5ce0d92d,0xc204e1ce,0x81c61440,0x1c62ca24, + 0xd04503ce,0x85d63944,0x75338e65,0x5d86075e, + 0x89e69647,0x75e76576, +]); +Lev2TParametricDescription.prototype.offsetIncrs5 = /*3 bits per value */ new Int32Array([ + 0x10000000,0xc00000,0x60061,0x400, + 0x0,0x60000008,0x6b003080,0xdb6ab6db, + 0x2db6,0x800400,0x49245240,0x11482412, + 0x104904,0x40020000,0x92292000,0xa4b25924, + 0x9649658,0xd80c000,0xdb0c001b,0x80db6d86, + 0x6db01b6d,0xc0600003,0x86000d86,0x6db6c36d, + 0xddadb6ed,0x300001b6,0x6c360,0xe37236e4, + 0x46db6236,0xdb6c,0x361b018,0xb91b7200, + 0x6dbb1b71,0x6db763,0x20100820,0x61248001, + 0x92492490,0x24820004,0x8041000,0x92400090, + 0x24924830,0x555b6a49,0x2080012,0x20004804, + 0x49252449,0x84112492,0x4000928,0x240201, + 0x92922490,0x58924924,0x49456,0x120d8082, + 0x6da4800,0x69249249,0x249a01b,0x6c04100, + 0x6d240009,0x92492483,0x24d5adb4,0x60208001, + 0x92000483,0x24925236,0x6846da49,0x10400092, + 0x241b0,0x49291b49,0x636d2492,0x92494935, + 0x24924924,0x49249249,0x92492492,0x24924924, + 0x49249249,0x92492492,0x24924924,0x49249249, + 0x92492492,0x24924924,0x49249249,0x92492492, + 0x24924924,0x49249249,0x92492492,0x24924924, + 0x49249249,0x92492492,0x24924924,0x49249249, + 0x92492492,0x24924924,0x49249249,0x92492492, + 0x24924924,0x49249249,0x92492492,0x24924924, + 0x49249249,0x92492492,0x24924924,0x49249249, + 0x92492492,0x24924924,0x49249249,0x92492492, + 0x24924924,0x49249249,0x92492492,0x24924924, + 0x49249249,0x92492492,0x24924924,0x49249249, + 0x92492492,0x24924924,0x49249249,0x92492492, + 0x24924924,0x49249249,0x92492492,0x24924924, + 0x49249249,0x92492492,0x24924924,0x49249249, + 0x92492492,0x24924924,0x49249249,0x92492492, + 0x24924924,0x49249249,0x92492492,0x24924924, + 0x49249249,0x92492492,0x24924924, +]); + +class Lev1TParametricDescription extends ParametricDescription { + /** + * @param {number} absState + * @param {number} position + * @param {number} vector + * @returns {number} + */ + transition(absState, position, vector) { + let state = Math.floor(absState / (this.w + 1)); + let offset = absState % (this.w + 1); + + if (position === this.w) { + if (state < 2) { + const loc = Math.imul(vector, 2) + state; + offset += this.unpack(this.offsetIncrs0, loc, 1); + state = this.unpack(this.toStates0, loc, 2) - 1; + } + } else if (position === this.w - 1) { + if (state < 3) { + const loc = Math.imul(vector, 3) + state; + offset += this.unpack(this.offsetIncrs1, loc, 1); + state = this.unpack(this.toStates1, loc, 2) - 1; + } + } else if (position === this.w - 2) { + if (state < 6) { + const loc = Math.imul(vector, 6) + state; + offset += this.unpack(this.offsetIncrs2, loc, 2); + state = this.unpack(this.toStates2, loc, 3) - 1; + } + } else { + // eslint-disable-next-line no-lonely-if + if (state < 6) { + const loc = Math.imul(vector, 6) + state; + offset += this.unpack(this.offsetIncrs3, loc, 2); + state = this.unpack(this.toStates3, loc, 3) - 1; + } + } + + if (state === -1) { + // null state + return -1; + } else { + // translate back to abs + return Math.imul(state, this.w + 1) + offset; + } + } + + // state map + // 0 -> [(0, 0)] + // 1 -> [(0, 1)] + // 2 -> [(0, 1), (1, 1)] + // 3 -> [(0, 1), (1, 1), (2, 1)] + // 4 -> [(0, 1), (2, 1)] + // 5 -> [t(0, 1), (0, 1), (1, 1), (2, 1)] + + + /** @param {number} w - length of word being checked */ + constructor(w) { + super(w, 1, new Int32Array([0,1,0,-1,-1,-1])); + } +} + +Lev1TParametricDescription.prototype.toStates0 = /*2 bits per value */ new Int32Array([ + 0x2, +]); +Lev1TParametricDescription.prototype.offsetIncrs0 = /*1 bits per value */ new Int32Array([ + 0x0, +]); + +Lev1TParametricDescription.prototype.toStates1 = /*2 bits per value */ new Int32Array([ + 0xa43, +]); +Lev1TParametricDescription.prototype.offsetIncrs1 = /*1 bits per value */ new Int32Array([ + 0x38, +]); + +Lev1TParametricDescription.prototype.toStates2 = /*3 bits per value */ new Int32Array([ + 0x12180003,0xb45a4914,0x69, +]); +Lev1TParametricDescription.prototype.offsetIncrs2 = /*2 bits per value */ new Int32Array([ + 0x558a0000,0x5555, +]); + +Lev1TParametricDescription.prototype.toStates3 = /*3 bits per value */ new Int32Array([ + 0x900c0003,0xa1904864,0x45a49169,0x5a6d196a, + 0x9634, +]); +Lev1TParametricDescription.prototype.offsetIncrs3 = /*2 bits per value */ new Int32Array([ + 0xa0fc0000,0x5555ba08,0x55555555, +]); diff --git a/src/librustdoc/html/static/js/tsconfig.json b/src/librustdoc/html/static/js/tsconfig.json index b81099bb9dfd0..42993cb0f2ac1 100644 --- a/src/librustdoc/html/static/js/tsconfig.json +++ b/src/librustdoc/html/static/js/tsconfig.json @@ -10,6 +10,6 @@ "skipLibCheck": true }, "typeAcquisition": { - "include": ["./rustdoc.d.ts"] + "include": ["./rustdoc.d.ts", "./stringdex.d.ts"] } } diff --git a/src/librustdoc/html/static_files.rs b/src/librustdoc/html/static_files.rs index 45589a3706983..e670c2f39e72f 100644 --- a/src/librustdoc/html/static_files.rs +++ b/src/librustdoc/html/static_files.rs @@ -80,6 +80,7 @@ static_files! { normalize_css => "static/css/normalize.css", main_js => "static/js/main.js", search_js => "static/js/search.js", + stringdex_js => "static/js/stringdex.js", settings_js => "static/js/settings.js", src_script_js => "static/js/src-script.js", storage_js => "static/js/storage.js", diff --git a/src/librustdoc/html/templates/page.html b/src/librustdoc/html/templates/page.html index 7af99e7097c37..0f4e7a9424854 100644 --- a/src/librustdoc/html/templates/page.html +++ b/src/librustdoc/html/templates/page.html @@ -29,6 +29,7 @@ data-rustdoc-version="{{rustdoc_version}}" {#+ #} data-channel="{{rust_channel}}" {#+ #} data-search-js="{{files.search_js}}" {#+ #} + data-stringdex-js="{{files.stringdex_js}}" {#+ #} data-settings-js="{{files.settings_js}}" {#+ #} > {# #} <script src="{{static_root_path|safe}}{{files.storage_js}}"></script> @@ -72,18 +73,9 @@ <![endif]--> {{ layout.external_html.before_content|safe }} {% if page.css_class != "src" %} - <nav class="mobile-topbar"> {# #} - <button class="sidebar-menu-toggle" title="show sidebar"></button> - {% if !layout.logo.is_empty() || page.rust_logo %} - <a class="logo-container" href="{{page.root_path|safe}}{{display_krate_with_trailing_slash|safe}}index.html"> - {% if page.rust_logo %} - <img class="rust-logo" src="{{static_root_path|safe}}{{files.rust_logo_svg}}" alt=""> - {% else if !layout.logo.is_empty() %} - <img src="{{layout.logo}}" alt=""> - {% endif %} - </a> - {% endif %} - </nav> + <rustdoc-topbar> {# #} + <h2><a href="#">{{page.short_title}}</a></h2> {# #} + </rustdoc-topbar> {% endif %} <nav class="sidebar"> {% if page.css_class != "src" %} @@ -117,9 +109,6 @@ <h2>Files</h2> {# #} <div class="sidebar-resizer" title="Drag to resize sidebar"></div> {# #} <main> {% if page.css_class != "src" %}<div class="width-limiter">{% endif %} - {# defined in storage.js to avoid duplicating complex UI across every page #} - {# and because the search form only works if JS is enabled anyway #} - <rustdoc-search></rustdoc-search> {# #} <section id="main-content" class="content">{{ content|safe }}</section> {% if page.css_class != "src" %}</div>{% endif %} </main> diff --git a/src/librustdoc/html/templates/print_item.html b/src/librustdoc/html/templates/print_item.html index 62954dbb02323..640fd3dfee498 100644 --- a/src/librustdoc/html/templates/print_item.html +++ b/src/librustdoc/html/templates/print_item.html @@ -12,8 +12,8 @@ <h1> {{typ}} <span{% if item_type != "mod" +%} class="{{item_type}}"{% endif %}> - {{name}} - </span> {# #} + {{name|wrapped|safe}} + </span> {# #} <button id="copy-path" title="Copy item path to clipboard"> {# #} Copy item path {# #} </button> {# #} diff --git a/src/tools/rustdoc-js/tester.js b/src/tools/rustdoc-js/tester.js index 0baa179e16b2d..a1e632ce74334 100644 --- a/src/tools/rustdoc-js/tester.js +++ b/src/tools/rustdoc-js/tester.js @@ -1,7 +1,8 @@ /* global globalThis */ + const fs = require("fs"); const path = require("path"); - +const { isGeneratorObject } = require("util/types"); function arrayToCode(array) { return array.map((value, index) => { @@ -45,23 +46,16 @@ function shouldIgnoreField(fieldName) { } function valueMapper(key, testOutput) { - const isAlias = testOutput["is_alias"]; let value = testOutput[key]; // To make our life easier, if there is a "parent" type, we add it to the path. if (key === "path") { - if (testOutput["parent"] !== undefined) { + if (testOutput["parent"]) { if (value.length > 0) { value += "::" + testOutput["parent"]["name"]; } else { value = testOutput["parent"]["name"]; } - } else if (testOutput["is_alias"]) { - value = valueMapper(key, testOutput["original"]); } - } else if (isAlias && key === "alias") { - value = testOutput["name"]; - } else if (isAlias && ["name"].includes(key)) { - value = testOutput["original"][key]; } return value; } @@ -237,7 +231,7 @@ async function runSearch(query, expected, doSearch, loadedFile, queryName) { const ignore_order = loadedFile.ignore_order; const exact_check = loadedFile.exact_check; - const results = await doSearch(query, loadedFile.FILTER_CRATE); + const { resultsTable } = await doSearch(query, loadedFile.FILTER_CRATE); const error_text = []; for (const key in expected) { @@ -247,37 +241,38 @@ async function runSearch(query, expected, doSearch, loadedFile, queryName) { if (!Object.prototype.hasOwnProperty.call(expected, key)) { continue; } - if (!Object.prototype.hasOwnProperty.call(results, key)) { + if (!Object.prototype.hasOwnProperty.call(resultsTable, key)) { error_text.push("==> Unknown key \"" + key + "\""); break; } const entry = expected[key]; - if (exact_check && entry.length !== results[key].length) { + if (exact_check && entry.length !== resultsTable[key].length) { error_text.push(queryName + "==> Expected exactly " + entry.length + - " results but found " + results[key].length + " in '" + key + "'"); + " results but found " + resultsTable[key].length + " in '" + key + "'"); } let prev_pos = -1; for (const [index, elem] of entry.entries()) { - const entry_pos = lookForEntry(elem, results[key]); + const entry_pos = lookForEntry(elem, resultsTable[key]); if (entry_pos === -1) { error_text.push(queryName + "==> Result not found in '" + key + "': '" + JSON.stringify(elem) + "'"); // By default, we just compare the two first items. let item_to_diff = 0; - if ((!ignore_order || exact_check) && index < results[key].length) { + if ((!ignore_order || exact_check) && index < resultsTable[key].length) { item_to_diff = index; } error_text.push("Diff of first error:\n" + - betterLookingDiff(elem, results[key][item_to_diff])); + betterLookingDiff(elem, resultsTable[key][item_to_diff])); } else if (exact_check === true && prev_pos + 1 !== entry_pos) { error_text.push(queryName + "==> Exact check failed at position " + (prev_pos + 1) + ": expected '" + JSON.stringify(elem) + "' but found '" + - JSON.stringify(results[key][index]) + "'"); + JSON.stringify(resultsTable[key][index]) + "'"); } else if (ignore_order === false && entry_pos < prev_pos) { - error_text.push(queryName + "==> '" + JSON.stringify(elem) + "' was supposed " + - "to be before '" + JSON.stringify(results[key][prev_pos]) + "'"); + error_text.push(queryName + "==> '" + + JSON.stringify(elem) + "' was supposed to be before '" + + JSON.stringify(resultsTable[key][prev_pos]) + "'"); } else { prev_pos = entry_pos; } @@ -286,19 +281,20 @@ async function runSearch(query, expected, doSearch, loadedFile, queryName) { return error_text; } -async function runCorrections(query, corrections, getCorrections, loadedFile) { - const qc = await getCorrections(query, loadedFile.FILTER_CRATE); +async function runCorrections(query, corrections, doSearch, loadedFile) { + const { parsedQuery } = await doSearch(query, loadedFile.FILTER_CRATE); + const qc = parsedQuery.correction; const error_text = []; if (corrections === null) { if (qc !== null) { - error_text.push(`==> expected = null, found = ${qc}`); + error_text.push(`==> [correction] expected = null, found = ${qc}`); } return error_text; } - if (qc !== corrections.toLowerCase()) { - error_text.push(`==> expected = ${corrections}, found = ${qc}`); + if (qc.toLowerCase() !== corrections.toLowerCase()) { + error_text.push(`==> [correction] expected = ${corrections}, found = ${qc}`); } return error_text; @@ -320,7 +316,7 @@ function checkResult(error_text, loadedFile, displaySuccess) { return 1; } -async function runCheckInner(callback, loadedFile, entry, getCorrections, extra) { +async function runCheckInner(callback, loadedFile, entry, extra, doSearch) { if (typeof entry.query !== "string") { console.log("FAILED"); console.log("==> Missing `query` field"); @@ -338,7 +334,7 @@ async function runCheckInner(callback, loadedFile, entry, getCorrections, extra) error_text = await runCorrections( entry.query, entry.correction, - getCorrections, + doSearch, loadedFile, ); if (checkResult(error_text, loadedFile, false) !== 0) { @@ -348,16 +344,16 @@ async function runCheckInner(callback, loadedFile, entry, getCorrections, extra) return true; } -async function runCheck(loadedFile, key, getCorrections, callback) { +async function runCheck(loadedFile, key, doSearch, callback) { const expected = loadedFile[key]; if (Array.isArray(expected)) { for (const entry of expected) { - if (!await runCheckInner(callback, loadedFile, entry, getCorrections, true)) { + if (!await runCheckInner(callback, loadedFile, entry, true, doSearch)) { return 1; } } - } else if (!await runCheckInner(callback, loadedFile, expected, getCorrections, false)) { + } else if (!await runCheckInner(callback, loadedFile, expected, false, doSearch)) { return 1; } console.log("OK"); @@ -368,7 +364,7 @@ function hasCheck(content, checkName) { return content.startsWith(`const ${checkName}`) || content.includes(`\nconst ${checkName}`); } -async function runChecks(testFile, doSearch, parseQuery, getCorrections) { +async function runChecks(testFile, doSearch, parseQuery) { let checkExpected = false; let checkParsed = false; let testFileContent = readFile(testFile); @@ -397,12 +393,12 @@ async function runChecks(testFile, doSearch, parseQuery, getCorrections) { let res = 0; if (checkExpected) { - res += await runCheck(loadedFile, "EXPECTED", getCorrections, (query, expected, text) => { + res += await runCheck(loadedFile, "EXPECTED", doSearch, (query, expected, text) => { return runSearch(query, expected, doSearch, loadedFile, text); }); } if (checkParsed) { - res += await runCheck(loadedFile, "PARSED", getCorrections, (query, expected, text) => { + res += await runCheck(loadedFile, "PARSED", doSearch, (query, expected, text) => { return runParser(query, expected, parseQuery, text); }); } @@ -416,71 +412,89 @@ async function runChecks(testFile, doSearch, parseQuery, getCorrections) { * @param {string} resource_suffix - Version number between filename and .js, e.g. "1.59.0" * @returns {Object} - Object containing keys: `doSearch`, which runs a search * with the loaded index and returns a table of results; `parseQuery`, which is the - * `parseQuery` function exported from the search module; and `getCorrections`, which runs + * `parseQuery` function exported from the search module, which runs * a search but returns type name corrections instead of results. */ -function loadSearchJS(doc_folder, resource_suffix) { - const searchIndexJs = path.join(doc_folder, "search-index" + resource_suffix + ".js"); - const searchIndex = require(searchIndexJs); - - globalThis.searchState = { - descShards: new Map(), - loadDesc: async function({descShard, descIndex}) { - if (descShard.promise === null) { - descShard.promise = new Promise((resolve, reject) => { - descShard.resolve = resolve; - const ds = descShard; - const fname = `${ds.crate}-desc-${ds.shard}-${resource_suffix}.js`; - fs.readFile( - `${doc_folder}/search.desc/${descShard.crate}/${fname}`, - (err, data) => { - if (err) { - reject(err); - } else { - eval(data.toString("utf8")); - } - }, - ); - }); - } - const list = await descShard.promise; - return list[descIndex]; - }, - loadedDescShard: function(crate, shard, data) { - this.descShards.get(crate)[shard].resolve(data.split("\n")); - }, - }; - +async function loadSearchJS(doc_folder, resource_suffix) { const staticFiles = path.join(doc_folder, "static.files"); + const stringdexJs = fs.readdirSync(staticFiles).find(f => f.match(/stringdex.*\.js$/)); + const stringdexModule = require(path.join(staticFiles, stringdexJs)); const searchJs = fs.readdirSync(staticFiles).find(f => f.match(/search.*\.js$/)); const searchModule = require(path.join(staticFiles, searchJs)); - searchModule.initSearch(searchIndex.searchIndex); - const docSearch = searchModule.docSearch; + globalThis.nonnull = (x, msg) => { + if (x === null) { + throw (msg || "unexpected null value!"); + } else { + return x; + } + }; + const { docSearch, DocSearch } = await searchModule.initSearch( + stringdexModule.Stringdex, + stringdexModule.RoaringBitmap, + { + loadRoot: callbacks => { + for (const key in callbacks) { + if (Object.hasOwn(callbacks, key)) { + globalThis[key] = callbacks[key]; + } + } + const rootJs = readFile(path.join(doc_folder, "search.index/root" + + resource_suffix + ".js")); + eval(rootJs); + }, + loadTreeByHash: hashHex => { + const shardJs = readFile(path.join(doc_folder, "search.index/" + hashHex + ".js")); + eval(shardJs); + }, + loadDataByNameAndHash: (name, hashHex) => { + const shardJs = readFile(path.join(doc_folder, "search.index/" + name + "/" + + hashHex + ".js")); + eval(shardJs); + }, + }, + ); return { doSearch: async function(queryStr, filterCrate, currentCrate) { - const result = await docSearch.execQuery(searchModule.parseQuery(queryStr), - filterCrate, currentCrate); + const parsedQuery = DocSearch.parseQuery(queryStr); + const result = await docSearch.execQuery(parsedQuery, filterCrate, currentCrate); + const resultsTable = {}; for (const tab in result) { if (!Object.prototype.hasOwnProperty.call(result, tab)) { continue; } - if (!(result[tab] instanceof Array)) { + if (!isGeneratorObject(result[tab])) { continue; } - for (const entry of result[tab]) { + resultsTable[tab] = []; + for await (const entry of result[tab]) { + const flatEntry = Object.assign({ + crate: entry.item.crate, + name: entry.item.name, + path: entry.item.modulePath, + exactPath: entry.item.exactModulePath, + ty: entry.item.ty, + }, entry); for (const key in entry) { if (!Object.prototype.hasOwnProperty.call(entry, key)) { continue; } - if (key === "displayTypeSignature" && entry.displayTypeSignature !== null) { - const {type, mappedNames, whereClause} = - await entry.displayTypeSignature; - entry.displayType = arrayToCode(type); - entry.displayMappedNames = [...mappedNames.entries()] + if (key === "desc" && entry.desc !== null) { + flatEntry.desc = await entry.desc; + } else if (key === "displayTypeSignature" && + entry.displayTypeSignature !== null + ) { + flatEntry.displayTypeSignature = await entry.displayTypeSignature; + const { + type, + mappedNames, + whereClause, + } = flatEntry.displayTypeSignature; + flatEntry.displayType = arrayToCode(type); + flatEntry.displayMappedNames = [...mappedNames.entries()] .map(([name, qname]) => { return `${name} = ${qname}`; }).join(", "); - entry.displayWhereClause = [...whereClause.entries()] + flatEntry.displayWhereClause = [...whereClause.entries()] .flatMap(([name, value]) => { if (value.length === 0) { return []; @@ -489,16 +503,12 @@ function loadSearchJS(doc_folder, resource_suffix) { }).join(", "); } } + resultsTable[tab].push(flatEntry); } } - return result; + return { resultsTable, parsedQuery }; }, - getCorrections: function(queryStr, filterCrate, currentCrate) { - const parsedQuery = searchModule.parseQuery(queryStr); - docSearch.execQuery(parsedQuery, filterCrate, currentCrate); - return parsedQuery.correction; - }, - parseQuery: searchModule.parseQuery, + parseQuery: DocSearch.parseQuery, }; } @@ -570,7 +580,7 @@ async function main(argv) { return 1; } - const parseAndSearch = loadSearchJS( + const parseAndSearch = await loadSearchJS( opts["doc_folder"], opts["resource_suffix"], ); @@ -579,14 +589,11 @@ async function main(argv) { const doSearch = function(queryStr, filterCrate) { return parseAndSearch.doSearch(queryStr, filterCrate, opts["crate_name"]); }; - const getCorrections = function(queryStr, filterCrate) { - return parseAndSearch.getCorrections(queryStr, filterCrate, opts["crate_name"]); - }; if (opts["test_file"].length !== 0) { for (const file of opts["test_file"]) { process.stdout.write(`Testing ${file} ... `); - errors += await runChecks(file, doSearch, parseAndSearch.parseQuery, getCorrections); + errors += await runChecks(file, doSearch, parseAndSearch.parseQuery); } } else if (opts["test_folder"].length !== 0) { for (const file of fs.readdirSync(opts["test_folder"])) { @@ -595,7 +602,7 @@ async function main(argv) { } process.stdout.write(`Testing ${file} ... `); errors += await runChecks(path.join(opts["test_folder"], file), doSearch, - parseAndSearch.parseQuery, getCorrections); + parseAndSearch.parseQuery); } } return errors > 0 ? 1 : 0; diff --git a/tests/run-make/emit-shared-files/rmake.rs b/tests/run-make/emit-shared-files/rmake.rs index f88fe69aa9cd7..911ceb3adca4b 100644 --- a/tests/run-make/emit-shared-files/rmake.rs +++ b/tests/run-make/emit-shared-files/rmake.rs @@ -19,7 +19,7 @@ fn main() { .args(&["--extend-css", "z.css"]) .input("x.rs") .run(); - assert!(path("invocation-only/search-index-xxx.js").exists()); + assert!(path("invocation-only/search.index/root-xxx.js").exists()); assert!(path("invocation-only/crates-xxx.js").exists()); assert!(path("invocation-only/settings.html").exists()); assert!(path("invocation-only/x/all.html").exists()); diff --git a/tests/run-make/rustdoc-determinism/rmake.rs b/tests/run-make/rustdoc-determinism/rmake.rs index 921baef4a9790..5705dff6858c5 100644 --- a/tests/run-make/rustdoc-determinism/rmake.rs +++ b/tests/run-make/rustdoc-determinism/rmake.rs @@ -15,7 +15,7 @@ fn main() { rustdoc().input("foo.rs").out_dir(&bar_first).run(); diff() - .expected_file(foo_first.join("search-index.js")) - .actual_file(bar_first.join("search-index.js")) + .expected_file(foo_first.join("search.index/root.js")) + .actual_file(bar_first.join("search.index/root.js")) .run(); } diff --git a/tests/rustdoc-gui/code-example-buttons.goml b/tests/rustdoc-gui/code-example-buttons.goml index b96f6ddcc37aa..1429f978a28ae 100644 --- a/tests/rustdoc-gui/code-example-buttons.goml +++ b/tests/rustdoc-gui/code-example-buttons.goml @@ -5,25 +5,25 @@ include: "utils.goml" // First we check we "hover". move-cursor-to: ".example-wrap" assert-css: (".example-wrap .copy-button", { "visibility": "visible" }) -move-cursor-to: ".search-input" +move-cursor-to: "#search-button" assert-css: (".example-wrap .copy-button", { "visibility": "hidden" }) // Now we check the click. assert-count: (".example-wrap:not(:hover) .button-holder.keep-visible", 0) click: ".example-wrap" -move-cursor-to: ".search-input" +move-cursor-to: "#search-button" // It should have a new class and be visible. wait-for-count: (".example-wrap:not(:hover) .button-holder.keep-visible", 1) wait-for-css: (".example-wrap:not(:hover) .button-holder.keep-visible", { "visibility": "visible" }) // Clicking again will remove the class. click: ".example-wrap" -move-cursor-to: ".search-input" +move-cursor-to: "rustdoc-toolbar #search-button" assert-count: (".example-wrap:not(:hover) .button-holder.keep-visible", 0) assert-css: (".example-wrap .copy-button", { "visibility": "hidden" }) // Clicking on the "copy code" button shouldn't make the buttons stick. click: ".example-wrap .copy-button" -move-cursor-to: ".search-input" +move-cursor-to: "#search-button" assert-count: (".example-wrap:not(:hover) .button-holder.keep-visible", 0) assert-css: (".example-wrap .copy-button", { "visibility": "hidden" }) // Since we clicked on the copy button, the clipboard content should have been updated. diff --git a/tests/rustdoc-gui/copy-code.goml b/tests/rustdoc-gui/copy-code.goml index 9cc717bc67a3d..a6fb816c4bd68 100644 --- a/tests/rustdoc-gui/copy-code.goml +++ b/tests/rustdoc-gui/copy-code.goml @@ -12,7 +12,7 @@ define-function: ( assert-count: (".example-wrap .copy-button", 1) // We now ensure it's only displayed when the example is hovered. assert-css: (".example-wrap .copy-button", { "visibility": "visible" }) - move-cursor-to: ".search-input" + move-cursor-to: "rustdoc-toolbar #search-button" assert-css: (".example-wrap .copy-button", { "visibility": "hidden" }) // Checking that the copy button has the same size as the "copy path" button. compare-elements-size: ( diff --git a/tests/rustdoc-gui/cursor.goml b/tests/rustdoc-gui/cursor.goml index 9412987fc3238..2134fc920665f 100644 --- a/tests/rustdoc-gui/cursor.goml +++ b/tests/rustdoc-gui/cursor.goml @@ -8,6 +8,8 @@ assert-css: ("#toggle-all-docs", {"cursor": "pointer"}) assert-css: ("#copy-path", {"cursor": "pointer"}) // the search tabs +click: "#search-button" +wait-for: ".search-input" write-into: (".search-input", "Foo") // To be SURE that the search will be run. press-key: 'Enter' diff --git a/tests/rustdoc-gui/docblock-code-block-line-number.goml b/tests/rustdoc-gui/docblock-code-block-line-number.goml index 0df9cc2a65983..a182124aced2f 100644 --- a/tests/rustdoc-gui/docblock-code-block-line-number.goml +++ b/tests/rustdoc-gui/docblock-code-block-line-number.goml @@ -69,7 +69,7 @@ call-function: ("check-colors", { // and make sure it goes away. // First, open the settings menu. -click: "#settings-menu" +click: "rustdoc-toolbar .settings-menu" wait-for: "#settings" assert-css: ("#settings", {"display": "block"}) @@ -121,7 +121,7 @@ call-function: ("check-padding", { define-function: ("check-line-numbers-existence", [], block { assert-local-storage: {"rustdoc-line-numbers": "true" } assert-false: ".example-line-numbers" - click: "#settings-menu" + click: "rustdoc-toolbar .settings-menu" wait-for: "#settings" // Then, click the toggle button. @@ -137,7 +137,7 @@ define-function: ("check-line-numbers-existence", [], block { // Line numbers should still be there. assert-css: ("[data-nosnippet]", { "display": "block"}) // Closing settings menu. - click: "#settings-menu" + click: "rustdoc-toolbar .settings-menu" wait-for-css: ("#settings", {"display": "none"}) }) @@ -168,7 +168,7 @@ assert: ".example-wrap > pre.rust" assert-count: (".example-wrap", 2) assert-count: (".example-wrap.digits-1", 2) -click: "#settings-menu" +click: "rustdoc-toolbar .settings-menu" wait-for: "#settings" // Then, click the toggle button. diff --git a/tests/rustdoc-gui/escape-key.goml b/tests/rustdoc-gui/escape-key.goml index ff8557b9b81ca..66d9c54fee4e0 100644 --- a/tests/rustdoc-gui/escape-key.goml +++ b/tests/rustdoc-gui/escape-key.goml @@ -2,10 +2,10 @@ // current content displayed. go-to: "file://" + |DOC_PATH| + "/test_docs/index.html" // First, we check that the search results are hidden when the Escape key is pressed. +click: "#search-button" +wait-for: "#search .search-switcher" // The search element is empty before the first search write-into: (".search-input", "test") -// To be SURE that the search will be run. -press-key: 'Enter' -wait-for: "#search h1" // The search element is empty before the first search +press-key: "Enter" // Check that the currently displayed element is search. wait-for: "#alternative-display #search" assert-attribute: ("#main-content", {"class": "content hidden"}) @@ -17,8 +17,8 @@ assert-false: "#alternative-display #search" assert-attribute: ("#main-content", {"class": "content"}) assert-document-property: ({"URL": "index.html"}, [ENDS_WITH]) -// Check that focusing the search input brings back the search results -focus: ".search-input" +// Check that clicking the search button brings back the search results +click: "#search-button" wait-for: "#alternative-display #search" assert-attribute: ("#main-content", {"class": "content hidden"}) assert-document-property: ({"URL": "index.html?search=test"}, ENDS_WITH) diff --git a/tests/rustdoc-gui/font-serif-change.goml b/tests/rustdoc-gui/font-serif-change.goml index b14d5ae96f920..1e9f21c35416d 100644 --- a/tests/rustdoc-gui/font-serif-change.goml +++ b/tests/rustdoc-gui/font-serif-change.goml @@ -8,7 +8,7 @@ assert-css: ("body", {"font-family": |serif_font|}) assert-css: ("p code", {"font-family": |serif_code_font|}) // We now switch to the sans serif font -click: "#settings-menu" +click: "main .settings-menu" wait-for: "#sans-serif-fonts" click: "#sans-serif-fonts" @@ -23,7 +23,7 @@ assert-css: ("body", {"font-family": |font|}) assert-css: ("p code", {"font-family": |code_font|}) // We switch back to the serif font -click: "#settings-menu" +click: "main .settings-menu" wait-for: "#sans-serif-fonts" click: "#sans-serif-fonts" diff --git a/tests/rustdoc-gui/globals.goml b/tests/rustdoc-gui/globals.goml index 7a0e2b9eb7462..6a56951e945e8 100644 --- a/tests/rustdoc-gui/globals.goml +++ b/tests/rustdoc-gui/globals.goml @@ -9,6 +9,7 @@ assert-window-property-false: {"searchIndex": null} // Form input go-to: "file://" + |DOC_PATH| + "/test_docs/index.html" +click: "#search-button" write-into: (".search-input", "Foo") press-key: 'Enter' wait-for: "#search-tabs" diff --git a/tests/rustdoc-gui/help-page.goml b/tests/rustdoc-gui/help-page.goml index 6d6e353ae362b..34b4081402766 100644 --- a/tests/rustdoc-gui/help-page.goml +++ b/tests/rustdoc-gui/help-page.goml @@ -6,12 +6,12 @@ assert-css: ("#help", {"display": "block"}) assert-css: ("#help dd", {"font-size": "16px"}) assert-false: "#help-button > a" assert-css: ("#help", {"display": "block"}) -compare-elements-property: (".sub", "#help", ["offsetWidth"]) -compare-elements-position: (".sub", "#help", ["x"]) +compare-elements-property: (".main-heading", "#help", ["offsetWidth"]) +compare-elements-position: (".main-heading", "#help", ["x"]) set-window-size: (500, 1000) // Try mobile next. assert-css: ("#help", {"display": "block"}) -compare-elements-property: (".sub", "#help", ["offsetWidth"]) -compare-elements-position: (".sub", "#help", ["x"]) +compare-elements-property: (".main-heading", "#help", ["offsetWidth"]) +compare-elements-position: (".main-heading", "#help", ["x"]) // Checking the color of the elements of the help menu. show-text: true @@ -54,19 +54,17 @@ go-to: "file://" + |DOC_PATH| + "/test_docs/index.html?search=a" wait-for: "#search-tabs" // Waiting for the search.js to load. set-window-size: (1000, 1000) // Only supported on desktop. assert-false: "#help" -click: "#help-button > a" +click: "rustdoc-toolbar .help-menu > a" assert-css: ("#help", {"display": "block"}) assert-css: ("#help dd", {"font-size": "16px"}) -click: "#help-button > a" -assert-css: ("#help", {"display": "none"}) -compare-elements-property-false: (".sub", "#help", ["offsetWidth"]) -compare-elements-position-false: (".sub", "#help", ["x"]) +click: "rustdoc-toolbar .help-menu > a" +assert-false: "#help" // This test ensures that the "the rustdoc book" anchor link within the help popover works. go-to: "file://" + |DOC_PATH| + "/test_docs/index.html?search=a" wait-for: "#search-tabs" // Waiting for the search.js to load. set-window-size: (1000, 1000) // Popover only appears when the screen width is >700px. assert-false: "#help" -click: "#help-button > a" +click: "rustdoc-toolbar .help-menu > a" click: "//*[@id='help']//a[text()='the rustdoc book']" wait-for-document-property: ({"URL": "https://doc.rust-lang.org/"}, STARTS_WITH) diff --git a/tests/rustdoc-gui/hide-mobile-topbar.goml b/tests/rustdoc-gui/hide-mobile-topbar.goml index 46eb8acfe8cd1..1e46d23582793 100644 --- a/tests/rustdoc-gui/hide-mobile-topbar.goml +++ b/tests/rustdoc-gui/hide-mobile-topbar.goml @@ -1,20 +1,19 @@ // Checks sidebar resizing stays synced with the setting -go-to: "file://" + |DOC_PATH| + "/test_docs/index.html" +go-to: "file://" + |DOC_PATH| + "/settings.html" set-window-size: (400, 600) // Verify that the "hide" option is unchecked -click: "#settings-menu" wait-for: "#settings" assert-css: ("#settings", {"display": "block"}) assert-property: ("#hide-sidebar", {"checked": "false"}) -assert-css: (".mobile-topbar", {"display": "flex"}) +assert-css: ("rustdoc-topbar", {"display": "flex"}) // Toggle it click: "#hide-sidebar" assert-property: ("#hide-sidebar", {"checked": "true"}) -assert-css: (".mobile-topbar", {"display": "none"}) +assert-css: ("rustdoc-topbar", {"display": "none"}) // Toggle it again click: "#hide-sidebar" assert-property: ("#hide-sidebar", {"checked": "false"}) -assert-css: (".mobile-topbar", {"display": "flex"}) +assert-css: ("rustdoc-topbar", {"display": "flex"}) diff --git a/tests/rustdoc-gui/huge-logo.goml b/tests/rustdoc-gui/huge-logo.goml index d207ab5bb37c2..6ad6948ef2ab1 100644 --- a/tests/rustdoc-gui/huge-logo.goml +++ b/tests/rustdoc-gui/huge-logo.goml @@ -8,8 +8,3 @@ assert-property: (".sidebar-crate .logo-container", {"offsetWidth": "96", "offse // offsetWidth = width of sidebar, offsetHeight = height + top padding assert-property: (".sidebar-crate .logo-container img", {"offsetWidth": "48", "offsetHeight": 64}) assert-css: (".sidebar-crate .logo-container img", {"border-top-width": "16px", "margin-top": "-16px"}) - -set-window-size: (400, 600) -// offset = size + margin -assert-property: (".mobile-topbar .logo-container", {"offsetWidth": "55", "offsetHeight": 45}) -assert-property: (".mobile-topbar .logo-container img", {"offsetWidth": "35", "offsetHeight": 35}) diff --git a/tests/rustdoc-gui/item-info.goml b/tests/rustdoc-gui/item-info.goml index 647a2fd290de0..12543ef3f4852 100644 --- a/tests/rustdoc-gui/item-info.goml +++ b/tests/rustdoc-gui/item-info.goml @@ -20,7 +20,7 @@ store-position: ( {"x": second_line_x, "y": second_line_y}, ) assert: |first_line_x| != |second_line_x| && |first_line_x| == 521 && |second_line_x| == 277 -assert: |first_line_y| != |second_line_y| && |first_line_y| == 718 && |second_line_y| == 741 +assert: |first_line_y| != |second_line_y| && |first_line_y| == 674 && |second_line_y| == 697 // Now we ensure that they're not rendered on the same line. set-window-size: (1100, 800) diff --git a/tests/rustdoc-gui/mobile-crate-name.goml b/tests/rustdoc-gui/mobile-crate-name.goml index a0c96eec8a5ad..524c1d36a8a87 100644 --- a/tests/rustdoc-gui/mobile-crate-name.goml +++ b/tests/rustdoc-gui/mobile-crate-name.goml @@ -5,18 +5,18 @@ go-to: "file://" + |DOC_PATH| + "/test_docs/index.html" // First we change the title to make it big. set-window-size: (350, 800) // We ensure that the "format" of the title is the same as the one we'll use. -assert-text: (".mobile-topbar .location a", "test_docs") +assert-text: ("rustdoc-topbar h2 a", "Crate test_docs") // We store the height we know is correct. -store-property: (".mobile-topbar .location", {"offsetHeight": height}) +store-property: ("rustdoc-topbar h2", {"offsetHeight": height}) // We change the crate name to something longer. -set-text: (".mobile-topbar .location a", "cargo_packager_resource_resolver") +set-text: ("rustdoc-topbar h2 a", "cargo_packager_resource_resolver") // And we check that the size remained the same. -assert-property: (".mobile-topbar .location", {"offsetHeight": |height|}) +assert-property: ("rustdoc-topbar h2", {"offsetHeight": |height|}) // Now we check if it works for the non-crate pages as well. go-to: "file://" + |DOC_PATH| + "/test_docs/struct.Foo.html" // We store the height we know is correct. -store-property: (".mobile-topbar .location", {"offsetHeight": height}) -set-text: (".mobile-topbar .location a", "Something_incredibly_long_because") +store-property: ("rustdoc-topbar h2", {"offsetHeight": height}) +set-text: ("rustdoc-topbar h2 a", "Something_incredibly_long_because") // And we check that the size remained the same. -assert-property: (".mobile-topbar .location", {"offsetHeight": |height|}) +assert-property: ("rustdoc-topbar h2", {"offsetHeight": |height|}) diff --git a/tests/rustdoc-gui/mobile.goml b/tests/rustdoc-gui/mobile.goml index a9eee53dd1d50..d292281932d73 100644 --- a/tests/rustdoc-gui/mobile.goml +++ b/tests/rustdoc-gui/mobile.goml @@ -5,7 +5,7 @@ set-window-size: (400, 600) set-font-size: 18 wait-for: 100 // wait a bit for the resize and the font-size change to be fully taken into account. -assert-property: (".mobile-topbar h2", {"offsetHeight": 33}) +assert-property: ("rustdoc-topbar h2", {"offsetHeight": 33}) // On the settings page, the theme buttons should not line-wrap. Instead, they should // all be placed as a group on a line below the setting name "Theme." diff --git a/tests/rustdoc-gui/notable-trait-failure.png b/tests/rustdoc-gui/notable-trait-failure.png new file mode 100644 index 0000000000000..24273c6d32626 Binary files /dev/null and b/tests/rustdoc-gui/notable-trait-failure.png differ diff --git a/tests/rustdoc-gui/notable-trait.goml b/tests/rustdoc-gui/notable-trait.goml index 7fc70e0675df4..6dbbe4365d317 100644 --- a/tests/rustdoc-gui/notable-trait.goml +++ b/tests/rustdoc-gui/notable-trait.goml @@ -82,15 +82,6 @@ call-function: ("check-notable-tooltip-position", { "i_x": 528, }) -// Checking on mobile now. -set-window-size: (650, 600) -wait-for-size: ("body", {"width": 650}) -call-function: ("check-notable-tooltip-position-complete", { - "x": 26, - "i_x": 305, - "popover_x": 0, -}) - // Now check the colors. define-function: ( "check-colors", @@ -176,6 +167,15 @@ call-function: ( }, ) +// Checking on mobile now. +set-window-size: (650, 600) +wait-for-size: ("body", {"width": 650}) +call-function: ("check-notable-tooltip-position-complete", { + "x": 26, + "i_x": 305, + "popover_x": 0, +}) + reload: // Check that pressing escape works @@ -189,7 +189,7 @@ assert: "#method\.create_an_iterator_from_read .tooltip:focus" // Check that clicking outside works. click: "//*[@id='method.create_an_iterator_from_read']//*[@class='tooltip']" assert-count: ("//*[@class='tooltip popover']", 1) -click: ".search-input" +click: ".main-heading h1" assert-count: ("//*[@class='tooltip popover']", 0) assert-false: "#method\.create_an_iterator_from_read .tooltip:focus" @@ -219,14 +219,14 @@ define-function: ( store-window-property: {"scrollY": scroll} click: "//*[@id='method.create_an_iterator_from_read']//*[@class='tooltip']" wait-for: "//*[@class='tooltip popover']" - click: "#settings-menu a" + click: ".main-heading h1" } ) // Now we check that the focus isn't given back to the wrong item when opening // another popover. call-function: ("setup-popup", {}) -click: ".search-input" +click: ".main-heading h1" // We ensure we didn't come back to the previous focused item. assert-window-property-false: {"scrollY": |scroll|} @@ -251,7 +251,7 @@ reload: assert-count: ("//*[@class='tooltip popover']", 0) click: "//*[@id='method.create_an_iterator_from_read']//*[@class='tooltip']" assert-count: ("//*[@class='tooltip popover']", 1) -click: "#settings-menu a" +click: "rustdoc-toolbar .settings-menu a" wait-for: "#settings" assert-count: ("//*[@class='tooltip popover']", 0) assert-false: "#method\.create_an_iterator_from_read .tooltip:focus" diff --git a/tests/rustdoc-gui/pocket-menu.goml b/tests/rustdoc-gui/pocket-menu.goml index 073172dd8a791..a0815bfa9a09a 100644 --- a/tests/rustdoc-gui/pocket-menu.goml +++ b/tests/rustdoc-gui/pocket-menu.goml @@ -3,33 +3,33 @@ include: "utils.goml" go-to: "file://" + |DOC_PATH| + "/test_docs/index.html?search=test" wait-for: "#crate-search" // First we check that the help menu doesn't exist yet. -assert-false: "#help-button .popover" +assert-false: "rustdoc-toolbar .help-menu .popover" // Then we display the help menu. -click: "#help-button" -assert: "#help-button .popover" -assert-css: ("#help-button .popover", {"display": "block"}) +click: "rustdoc-toolbar .help-menu" +assert: "rustdoc-toolbar .help-menu .popover" +assert-css: ("rustdoc-toolbar .help-menu .popover", {"display": "block"}) // Now we click somewhere else on the page to ensure it is handling the blur event // correctly. click: ".sidebar" -assert-css: ("#help-button .popover", {"display": "none"}) +assert-false: "rustdoc-toolbar .help-menu .popover" // Now we will check that we cannot have two "pocket menus" displayed at the same time. -click: "#help-button" -assert-css: ("#help-button .popover", {"display": "block"}) -click: "#settings-menu" -assert-css: ("#help-button .popover", {"display": "none"}) -assert-css: ("#settings-menu .popover", {"display": "block"}) +click: "rustdoc-toolbar .help-menu" +assert-css: ("rustdoc-toolbar .help-menu .popover", {"display": "block"}) +click: "rustdoc-toolbar .settings-menu" +assert-false: "rustdoc-toolbar .help-menu .popover" +assert-css: ("rustdoc-toolbar .settings-menu .popover", {"display": "block"}) // Now the other way. -click: "#help-button" -assert-css: ("#help-button .popover", {"display": "block"}) -assert-css: ("#settings-menu .popover", {"display": "none"}) +click: "rustdoc-toolbar .help-menu" +assert-css: ("rustdoc-toolbar .help-menu .popover", {"display": "block"}) +assert-css: ("rustdoc-toolbar .settings-menu .popover", {"display": "none"}) // Now verify that clicking the help menu again closes it. -click: "#help-button" -assert-css: ("#help-button .popover", {"display": "none"}) -assert-css: ("#settings-menu .popover", {"display": "none"}) +click: "rustdoc-toolbar .help-menu" +assert-false: "rustdoc-toolbar .help-menu .popover" +assert-css: (".settings-menu .popover", {"display": "none"}) define-function: ( "check-popover-colors", @@ -37,13 +37,21 @@ define-function: ( block { call-function: ("switch-theme", {"theme": |theme|}) - click: "#help-button" + click: "rustdoc-toolbar .help-menu" assert-css: ( - "#help-button .popover", + "rustdoc-toolbar .help-menu .popover", {"display": "block", "border-color": |border_color|}, ) - compare-elements-css: ("#help-button .popover", "#help-button .top", ["border-color"]) - compare-elements-css: ("#help-button .popover", "#help-button .bottom", ["border-color"]) + compare-elements-css: ( + "rustdoc-toolbar .help-menu .popover", + "rustdoc-toolbar .help-menu .top", + ["border-color"], + ) + compare-elements-css: ( + "rustdoc-toolbar .help-menu .popover", + "rustdoc-toolbar .help-menu .bottom", + ["border-color"], + ) } ) @@ -63,8 +71,21 @@ call-function: ("check-popover-colors", { // Opening the mobile sidebar should close the settings popover. set-window-size: (650, 600) -click: "#settings-menu a" -assert-css: ("#settings-menu .popover", {"display": "block"}) +click: "rustdoc-topbar .settings-menu a" +assert-css: ("rustdoc-topbar .settings-menu .popover", {"display": "block"}) +click: ".sidebar-menu-toggle" +assert: "//*[@class='sidebar shown']" +assert-css: ("rustdoc-topbar .settings-menu .popover", {"display": "none"}) +// Opening the settings popover should close the sidebar. +click: ".settings-menu a" +assert-css: ("rustdoc-topbar .settings-menu .popover", {"display": "block"}) +assert-false: "//*[@class='sidebar shown']" + +// Opening the settings popover at start (which async loads stuff) should also close. +reload: click: ".sidebar-menu-toggle" assert: "//*[@class='sidebar shown']" -assert-css: ("#settings-menu .popover", {"display": "none"}) +assert-false: "rustdoc-topbar .settings-menu .popover" +click: "rustdoc-topbar .settings-menu a" +assert-false: "//*[@class='sidebar shown']" +wait-for: "rustdoc-topbar .settings-menu .popover" diff --git a/tests/rustdoc-gui/scrape-examples-color.goml b/tests/rustdoc-gui/scrape-examples-color.goml index b0faca190a578..c84fe1f341118 100644 --- a/tests/rustdoc-gui/scrape-examples-color.goml +++ b/tests/rustdoc-gui/scrape-examples-color.goml @@ -27,7 +27,7 @@ define-function: ( "color": |help_hover_color|, }) // Moving the cursor to another item to not break next runs. - move-cursor-to: ".search-input" + move-cursor-to: "#search-button" } ) diff --git a/tests/rustdoc-gui/scrape-examples-layout.goml b/tests/rustdoc-gui/scrape-examples-layout.goml index 85a3b2a628734..681d0c24c6d85 100644 --- a/tests/rustdoc-gui/scrape-examples-layout.goml +++ b/tests/rustdoc-gui/scrape-examples-layout.goml @@ -64,8 +64,8 @@ assert-size: (".more-scraped-examples .scraped-example .example-wrap", { store-value: (offset_y, 4) // First with desktop -assert-position: (".scraped-example", {"y": 256}) -assert-position: (".scraped-example .prev", {"y": 256 + |offset_y|}) +assert-position: (".scraped-example", {"y": 214}) +assert-position: (".scraped-example .prev", {"y": 214 + |offset_y|}) // Gradient background should be at the top of the code block. assert-css: (".scraped-example .example-wrap::before", {"top": "0px"}) @@ -74,8 +74,8 @@ assert-css: (".scraped-example .example-wrap::after", {"bottom": "0px"}) // Then with mobile set-window-size: (600, 600) store-size: (".scraped-example .scraped-example-title", {"height": title_height}) -assert-position: (".scraped-example", {"y": 291}) -assert-position: (".scraped-example .prev", {"y": 291 + |offset_y| + |title_height|}) +assert-position: (".scraped-example", {"y": 249}) +assert-position: (".scraped-example .prev", {"y": 249 + |offset_y| + |title_height|}) define-function: ( "check_title_and_code_position", diff --git a/tests/rustdoc-gui/scrape-examples-toggle.goml b/tests/rustdoc-gui/scrape-examples-toggle.goml index 441895a7c0ee2..ec5710fbcdcd2 100644 --- a/tests/rustdoc-gui/scrape-examples-toggle.goml +++ b/tests/rustdoc-gui/scrape-examples-toggle.goml @@ -25,7 +25,7 @@ define-function: ( // We put the toggle in the original state. click: ".more-examples-toggle" // Moving cursor away from the toggle line to prevent disrupting next test. - move-cursor-to: ".search-input" + move-cursor-to: "rustdoc-toolbar #search-button" }, ) diff --git a/tests/rustdoc-gui/search-about-this-result.goml b/tests/rustdoc-gui/search-about-this-result.goml index 1d45c06dc43d4..ec1df737c8150 100644 --- a/tests/rustdoc-gui/search-about-this-result.goml +++ b/tests/rustdoc-gui/search-about-this-result.goml @@ -7,6 +7,7 @@ focus: ".search-input" press-key: "Enter" wait-for: "#search-tabs" +wait-for-false: "#search-tabs .count.loading" assert-count: ("#search-tabs button", 1) assert-count: (".search-results > a", 1) @@ -32,6 +33,7 @@ focus: ".search-input" press-key: "Enter" wait-for: "#search-tabs" +wait-for-false: "#search-tabs .count.loading" assert-text: ("//div[@class='type-signature']", "F -> WhereWhitespace<T>") assert-count: ("#search-tabs button", 1) assert-count: (".search-results > a", 1) diff --git a/tests/rustdoc-gui/search-corrections.goml b/tests/rustdoc-gui/search-corrections.goml index f80675730c41a..bd02b204f2ea3 100644 --- a/tests/rustdoc-gui/search-corrections.goml +++ b/tests/rustdoc-gui/search-corrections.goml @@ -3,99 +3,97 @@ // Checks that the search tab result tell the user about corrections // First, try a search-by-name go-to: "file://" + |DOC_PATH| + "/test_docs/index.html" +click: "#search-button" +wait-for: ".search-input" // Intentionally wrong spelling of "NotableStructWithLongName" write-into: (".search-input", "NotableStructWithLongNamr") // To be SURE that the search will be run. press-key: 'Enter' // Waiting for the search results to appear... wait-for: "#search-tabs" +wait-for-false: "#search-tabs .count.loading" // Corrections aren't shown on the "In Names" tab. assert: "#search-tabs button.selected:first-child" -assert-css: (".search-corrections", { - "display": "none" -}) +assert-false: ".search-results:nth-child(1) .search-corrections" // Corrections do get shown on the "In Parameters" tab. click: "#search-tabs button:nth-child(2)" assert: "#search-tabs button.selected:nth-child(2)" -assert-css: (".search-corrections", { - "display": "block" -}) assert-text: ( - ".search-corrections", - "Type \"NotableStructWithLongNamr\" not found. Showing results for closest type name \"notablestructwithlongname\" instead." + ".search-results:nth-child(2) .search-corrections", + "Type \"NotableStructWithLongNamr\" not found. Showing results for closest type name \"NotableStructWithLongName\" instead." ) // Corrections do get shown on the "In Return Type" tab. click: "#search-tabs button:nth-child(3)" assert: "#search-tabs button.selected:nth-child(3)" -assert-css: (".search-corrections", { - "display": "block" -}) assert-text: ( - ".search-corrections", - "Type \"NotableStructWithLongNamr\" not found. Showing results for closest type name \"notablestructwithlongname\" instead." + ".search-results:nth-child(3) .search-corrections", + "Type \"NotableStructWithLongNamr\" not found. Showing results for closest type name \"NotableStructWithLongName\" instead." ) // Now, explicit return values go-to: "file://" + |DOC_PATH| + "/test_docs/index.html" +click: "#search-button" +wait-for: ".search-input" // Intentionally wrong spelling of "NotableStructWithLongName" write-into: (".search-input", "-> NotableStructWithLongNamr") // To be SURE that the search will be run. press-key: 'Enter' // Waiting for the search results to appear... wait-for: "#search-tabs" +wait-for-false: "#search-tabs .count.loading" -assert-css: (".search-corrections", { - "display": "block" -}) assert-text: ( - ".search-corrections", - "Type \"NotableStructWithLongNamr\" not found. Showing results for closest type name \"notablestructwithlongname\" instead." + ".search-results.active .search-corrections", + "Type \"NotableStructWithLongNamr\" not found and used as generic parameter. Consider searching for \"NotableStructWithLongName\" instead." ) // Now, generic correction go-to: "file://" + |DOC_PATH| + "/test_docs/index.html" +click: "#search-button" +wait-for: ".search-input" // Intentionally wrong spelling of "NotableStructWithLongName" write-into: (".search-input", "NotableStructWithLongNamr, NotableStructWithLongNamr") // To be SURE that the search will be run. press-key: 'Enter' // Waiting for the search results to appear... wait-for: "#search-tabs" +wait-for-false: "#search-tabs .count.loading" -assert-css: (".search-corrections", { - "display": "block" -}) assert-text: ( - ".search-corrections", - "Type \"NotableStructWithLongNamr\" not found and used as generic parameter. Consider searching for \"notablestructwithlongname\" instead." + ".search-failed.active .search-corrections", + "Type \"NotableStructWithLongNamr\" not found and used as generic parameter. Consider searching for \"NotableStructWithLongName\" instead." ) // Now, generic correction plus error go-to: "file://" + |DOC_PATH| + "/test_docs/index.html" +click: "#search-button" +wait-for: ".search-input" // Intentionally wrong spelling of "NotableStructWithLongName" write-into: (".search-input", "Foo<NotableStructWithLongNamr>,y") // To be SURE that the search will be run. press-key: 'Enter' // Waiting for the search results to appear... wait-for: "#search-tabs" +wait-for-false: "#search-tabs .count.loading" -assert-css: (".search-corrections", { - "display": "block" -}) assert-text: ( - ".search-corrections", - "Type \"NotableStructWithLongNamr\" not found and used as generic parameter. Consider searching for \"notablestructwithlongname\" instead." + ".search-failed.active .search-corrections", + "Type \"NotableStructWithLongNamr\" not found and used as generic parameter. Consider searching for \"NotableStructWithLongName\" instead." ) go-to: "file://" + |DOC_PATH| + "/test_docs/index.html" +click: "#search-button" +wait-for: ".search-input" // Intentionally wrong spelling of "NotableStructWithLongName" write-into: (".search-input", "generic:NotableStructWithLongNamr<x>,y") // To be SURE that the search will be run. press-key: 'Enter' // Waiting for the search results to appear... wait-for: "#search-tabs" +wait-for-false: "#search-tabs .count.loading" assert-css: (".error", { "display": "block" diff --git a/tests/rustdoc-gui/search-error.goml b/tests/rustdoc-gui/search-error.goml index 4dc60669c7a58..4d7c2263fd123 100644 --- a/tests/rustdoc-gui/search-error.goml +++ b/tests/rustdoc-gui/search-error.goml @@ -8,6 +8,7 @@ define-function: ( [theme, error_background], block { call-function: ("switch-theme", {"theme": |theme|}) + wait-for-false: "#search-tabs .count.loading" wait-for: "#search .error code" assert-css: ("#search .error code", {"background-color": |error_background|}) } diff --git a/tests/rustdoc-gui/search-filter.goml b/tests/rustdoc-gui/search-filter.goml index c5038e0892b04..fcbc4363ea411 100644 --- a/tests/rustdoc-gui/search-filter.goml +++ b/tests/rustdoc-gui/search-filter.goml @@ -2,11 +2,14 @@ include: "utils.goml" go-to: "file://" + |DOC_PATH| + "/test_docs/index.html" show-text: true +click: "#search-button" +wait-for: ".search-input" write-into: (".search-input", "test") // To be SURE that the search will be run. press-key: 'Enter' // Waiting for the search results to appear... wait-for: "#search-tabs" +wait-for-false: "#search-tabs .count.loading" assert-text: ("#results .externcrate", "test_docs") wait-for: "#crate-search" @@ -21,6 +24,7 @@ press-key: "ArrowDown" press-key: "Enter" // Waiting for the search results to appear... wait-for: "#search-tabs" +wait-for-false: "#search-tabs .count.loading" assert-document-property: ({"URL": "&filter-crate="}, CONTAINS) // We check that there is no more "test_docs" appearing. assert-false: "#results .externcrate" @@ -31,7 +35,8 @@ assert-property: ("#crate-search", {"value": "lib2"}) // crate filtering. press-key: "Escape" wait-for-css: ("#main-content", {"display": "block"}) -focus: ".search-input" +click: "#search-button" +wait-for: ".search-input" wait-for-css: ("#main-content", {"display": "none"}) // We check that there is no more "test_docs" appearing. assert-false: "#results .externcrate" @@ -47,6 +52,7 @@ press-key: "ArrowUp" press-key: "Enter" // Waiting for the search results to appear... wait-for: "#search-tabs" +wait-for-false: "#search-tabs .count.loading" assert-property: ("#crate-search", {"value": "all crates"}) // Checking that the URL parameter is taken into account for crate filtering. @@ -56,8 +62,7 @@ assert-property: ("#crate-search", {"value": "lib2"}) assert-false: "#results .externcrate" // Checking that the text for the "title" is correct (the "all crates" comes from the "<select>"). -assert-text: (".search-results-title", "Results", STARTS_WITH) -assert-text: (".search-results-title + .sub-heading", " in all crates", STARTS_WITH) +assert-text: (".search-switcher", "Search results in all crates", STARTS_WITH) // Checking the display of the crate filter. // We start with the light theme. @@ -72,7 +77,7 @@ assert-css: ("#crate-search", { }) // We now check the dark theme. -click: "#settings-menu" +click: "rustdoc-toolbar .settings-menu" wait-for: "#settings" click: "#theme-dark" wait-for-css: ("#crate-search", { diff --git a/tests/rustdoc-gui/search-form-elements.goml b/tests/rustdoc-gui/search-form-elements.goml index efe39f7a9d1bf..fdf0afb7e8f72 100644 --- a/tests/rustdoc-gui/search-form-elements.goml +++ b/tests/rustdoc-gui/search-form-elements.goml @@ -2,6 +2,7 @@ include: "utils.goml" go-to: "file://" + |DOC_PATH| + "/test_docs/index.html?search=test" wait-for: "#search-tabs" // Waiting for the search.js to load. +wait-for-false: "#search-tabs .count.loading" show-text: true define-function: ( @@ -31,7 +32,7 @@ define-function: ( }, ) assert-css: ( - "#help-button > a", + "rustdoc-toolbar .help-menu > a", { "color": |menu_button_a_color|, "border-color": "transparent", @@ -39,9 +40,9 @@ define-function: ( }, ) // Hover help button. - move-cursor-to: "#help-button" + move-cursor-to: "rustdoc-toolbar .help-menu" assert-css: ( - "#help-button > a", + "rustdoc-toolbar .help-menu > a", { "color": |menu_button_a_color|, "border-color": |menu_button_a_border_hover|, @@ -49,15 +50,15 @@ define-function: ( }, ) // Link color inside - click: "#help-button" + click: "rustdoc-toolbar .help-menu" assert-css: ( - "#help a", + "rustdoc-toolbar #help a", { "color": |menu_a_color|, }, ) assert-css: ( - "#settings-menu > a", + "rustdoc-toolbar .settings-menu > a", { "color": |menu_button_a_color|, "border-color": "transparent", @@ -65,9 +66,9 @@ define-function: ( }, ) // Hover settings menu. - move-cursor-to: "#settings-menu" + move-cursor-to: "rustdoc-toolbar .settings-menu" assert-css: ( - "#settings-menu:hover > a", + "rustdoc-toolbar .settings-menu:hover > a", { "color": |menu_button_a_color|, "border-color": |menu_button_a_border_hover|, @@ -120,8 +121,10 @@ call-function: ( // Check that search input correctly decodes form encoding. go-to: "file://" + |DOC_PATH| + "/test_docs/index.html?search=a+b" wait-for: "#search-tabs" // Waiting for the search.js to load. +wait-for-false: "#search-tabs .count.loading" assert-property: (".search-input", { "value": "a b" }) // Check that literal + is not treated as space. go-to: "file://" + |DOC_PATH| + "/test_docs/index.html?search=a%2Bb" wait-for: "#search-tabs" // Waiting for the search.js to load. +wait-for-false: "#search-tabs .count.loading" assert-property: (".search-input", { "value": "a+b" }) diff --git a/tests/rustdoc-gui/search-input-mobile.goml b/tests/rustdoc-gui/search-input-mobile.goml index adcb3658a2702..a383d92d28802 100644 --- a/tests/rustdoc-gui/search-input-mobile.goml +++ b/tests/rustdoc-gui/search-input-mobile.goml @@ -2,10 +2,13 @@ // The PR which fixed it is: https://github.com/rust-lang/rust/pull/81592 go-to: "file://" + |DOC_PATH| + "/test_docs/index.html" set-window-size: (463, 700) -// We first check that the search input isn't already focused. -assert-false: ("input.search-input:focus") -click: "input.search-input" +click: "#search-button" +wait-for: ".search-input" +assert: "input.search-input:focus" + +go-to: "file://" + |DOC_PATH| + "/test_docs/index.html" reload: set-window-size: (750, 700) -click: "input.search-input" -assert: ("input.search-input:focus") +click: "#search-button" +wait-for: ".search-input" +assert: "input.search-input:focus" diff --git a/tests/rustdoc-gui/search-keyboard.goml b/tests/rustdoc-gui/search-keyboard.goml index 707bb8f5faa82..47474bf164313 100644 --- a/tests/rustdoc-gui/search-keyboard.goml +++ b/tests/rustdoc-gui/search-keyboard.goml @@ -1,28 +1,31 @@ // Checks that the search tab results work correctly with function signature syntax // First, try a search-by-name go-to: "file://" + |DOC_PATH| + "/test_docs/index.html" +click: "#search-button" +wait-for: ".search-input" write-into: (".search-input", "Foo") // To be SURE that the search will be run. press-key: 'Enter' // Waiting for the search results to appear... wait-for: "#search-tabs" +wait-for-false: "#search-tabs .count.loading" // Now use the keyboard commands to switch to the third result. press-key: "ArrowDown" press-key: "ArrowDown" press-key: "ArrowDown" -assert: ".search-results.active > a:focus:nth-of-type(3)" +wait-for: ".search-results.active > a:focus:nth-of-type(3)" // Now switch to the second tab, then back to the first one, then arrow back up. press-key: "ArrowRight" -assert: ".search-results.active:nth-of-type(2) > a:focus:nth-of-type(1)" +wait-for: ".search-results.active:nth-of-type(2) > a:focus:nth-of-type(1)" press-key: "ArrowLeft" -assert: ".search-results.active:nth-of-type(1) > a:focus:nth-of-type(3)" +wait-for: ".search-results.active:nth-of-type(1) > a:focus:nth-of-type(3)" press-key: "ArrowUp" -assert: ".search-results.active > a:focus:nth-of-type(2)" +wait-for: ".search-results.active > a:focus:nth-of-type(2)" press-key: "ArrowUp" -assert: ".search-results.active > a:focus:nth-of-type(1)" +wait-for: ".search-results.active > a:focus:nth-of-type(1)" press-key: "ArrowUp" -assert: ".search-input:focus" +wait-for: ".search-input:focus" press-key: "ArrowDown" -assert: ".search-results.active > a:focus:nth-of-type(1)" +wait-for: ".search-results.active > a:focus:nth-of-type(1)" diff --git a/tests/rustdoc-gui/search-reexport.goml b/tests/rustdoc-gui/search-reexport.goml index fa9eedeceac2e..aab657e6aa558 100644 --- a/tests/rustdoc-gui/search-reexport.goml +++ b/tests/rustdoc-gui/search-reexport.goml @@ -6,6 +6,8 @@ call-function: ("switch-theme", {"theme": "dark"}) // First we check that the reexport has the correct ID and no background color. assert-text: ("//*[@id='reexport.TheStdReexport']", "pub use ::std as TheStdReexport;") assert-css: ("//*[@id='reexport.TheStdReexport']", {"background-color": "rgba(0, 0, 0, 0)"}) +click: "#search-button" +wait-for: ".search-input" write-into: (".search-input", "TheStdReexport") // To be SURE that the search will be run. press-key: 'Enter' @@ -22,6 +24,8 @@ wait-for-css: ("//*[@id='reexport.TheStdReexport']", {"background-color": "#494a // We now check that the alias is working as well on the reexport. // To be SURE that the search will be run. press-key: 'Enter' +click: "#search-button" +wait-for: ".search-input" write-into: (".search-input", "AliasForTheStdReexport") wait-for: "//a[@class='result-import']" assert-text: ( diff --git a/tests/rustdoc-gui/search-result-color.goml b/tests/rustdoc-gui/search-result-color.goml index e6dd504d7036f..1820557ed397c 100644 --- a/tests/rustdoc-gui/search-result-color.goml +++ b/tests/rustdoc-gui/search-result-color.goml @@ -14,6 +14,7 @@ define-function: ( // Waiting for the search results to appear... wait-for: "#search-tabs" + wait-for-false: "#search-tabs .count.loading" assert-css: ( "#search-tabs > button > .count", {"color": |count_color|}, @@ -212,11 +213,14 @@ call-function: ("check-search-color", { // Check the alias. go-to: "file://" + |DOC_PATH| + "/test_docs/index.html" +click: "#search-button" +wait-for: ".search-input" write-into: (".search-input", "thisisanalias") // To be SURE that the search will be run. press-key: 'Enter' // Waiting for the search results to appear... wait-for: "#search-tabs" +wait-for-false: "#search-tabs .count.loading" define-function: ( "check-alias", diff --git a/tests/rustdoc-gui/search-result-description.goml b/tests/rustdoc-gui/search-result-description.goml index 745ef31e6cbe9..4ab250b472d0f 100644 --- a/tests/rustdoc-gui/search-result-description.goml +++ b/tests/rustdoc-gui/search-result-description.goml @@ -2,4 +2,5 @@ go-to: "file://" + |DOC_PATH| + "/test_docs/index.html?search=some_more_function" // Waiting for the search results to appear... wait-for: "#search-tabs" +wait-for-false: "#search-tabs .count.loading" assert-text: (".search-results .desc code", "format!") diff --git a/tests/rustdoc-gui/search-result-display.goml b/tests/rustdoc-gui/search-result-display.goml index 1521267956a88..a6272dfc78171 100644 --- a/tests/rustdoc-gui/search-result-display.goml +++ b/tests/rustdoc-gui/search-result-display.goml @@ -7,6 +7,7 @@ write-into: (".search-input", "test") // To be SURE that the search will be run. press-key: 'Enter' wait-for: "#crate-search" +wait-for-false: "#search-tabs .count.loading" // The width is returned by "getComputedStyle" which returns the exact number instead of the // CSS rule which is "50%"... assert-size: (".search-results div.desc", {"width": 248}) @@ -34,6 +35,7 @@ assert: |new_width| < |width| - 10 // Check that if the search is too long on mobile, it'll go under the "typename". go-to: "file://" + |DOC_PATH| + "/test_docs/index.html?search=SuperIncrediblyLongLongLongLongLongLongLongGigaGigaGigaMegaLongLongLongStructName" wait-for: "#crate-search" +wait-for-false: "#search-tabs .count.loading" compare-elements-position-near: ( ".search-results .result-name .typename", ".search-results .result-name .path", @@ -51,7 +53,7 @@ set-window-size: (900, 900) // First we check the current width, height and position. assert-css: ("#crate-search", {"width": "159px"}) -store-size: (".search-results-title", { +store-size: (".search-switcher", { "height": search_results_title_height, "width": search_results_title_width, }) @@ -64,8 +66,8 @@ set-text: ( ) // Then we compare again to confirm the height didn't change. -assert-size: ("#crate-search", {"width": 370}) -assert-size: (".search-results-title", { +assert-size: ("#crate-search", {"width": 179}) +assert-size: (".search-switcher", { "height": |search_results_title_height|, }) assert-css: ("#search", {"width": "640px"}) @@ -79,6 +81,7 @@ define-function: ( block { call-function: ("switch-theme", {"theme": |theme|}) wait-for: "#crate-search" + wait-for-false: "#search-tabs .count.loading" assert-css: ("#crate-search", {"border": "1px solid " + |border|}) assert-css: ("#crate-search-div::after", {"filter": |filter|}) move-cursor-to: "#crate-search" diff --git a/tests/rustdoc-gui/search-result-go-to-first.goml b/tests/rustdoc-gui/search-result-go-to-first.goml index 136213c517ebb..acb25453171d4 100644 --- a/tests/rustdoc-gui/search-result-go-to-first.goml +++ b/tests/rustdoc-gui/search-result-go-to-first.goml @@ -9,6 +9,7 @@ assert-text-false: (".main-heading h1", "Struct test_docs::FooCopy item path") go-to: "file://" + |DOC_PATH| + "/test_docs/index.html?search=struct%3AFoo" // Waiting for the search results to appear... wait-for: "#search-tabs" +wait-for-false: "#search-tabs .count.loading" assert-text-false: (".main-heading h1", "Struct test_docs::FooCopy item path") // Ensure that the search results are displayed, not the "normal" content. assert-css: ("#main-content", {"display": "none"}) @@ -17,4 +18,4 @@ assert-css: ("#main-content", {"display": "none"}) go-to: "file://" + |DOC_PATH| + "/test_docs/index.html?search=struct%3AFoo&go_to_first=true" // Waiting for the page to load... wait-for-text: (".main-heading .rustdoc-breadcrumbs", "test_docs") -wait-for-text: (".main-heading h1", "Struct FooCopy item path") +wait-for-text: (".main-heading h1", "Struct Foo Copy item path") diff --git a/tests/rustdoc-gui/search-result-impl-disambiguation.goml b/tests/rustdoc-gui/search-result-impl-disambiguation.goml index bca52b4649856..ca1c2aded5e63 100644 --- a/tests/rustdoc-gui/search-result-impl-disambiguation.goml +++ b/tests/rustdoc-gui/search-result-impl-disambiguation.goml @@ -5,11 +5,14 @@ go-to: "file://" + |DOC_PATH| + "/test_docs/index.html" // This should link to the inherent impl +click: "#search-button" +wait-for: ".search-input" write-into: (".search-input", "ZyxwvutMethodDisambiguation -> bool") // To be SURE that the search will be run. press-key: 'Enter' // Waiting for the search results to appear... wait-for: "#search-tabs" +wait-for-false: "#search-tabs .count.loading" // Check the disambiguated link. assert-count: ("a.result-method", 1) assert-attribute: ("a.result-method", { @@ -25,11 +28,14 @@ assert: "section:target" go-to: "file://" + |DOC_PATH| + "/test_docs/index.html" // This should link to the trait impl +click: "#search-button" +wait-for: ".search-input" write-into: (".search-input", "ZyxwvutMethodDisambiguation, usize -> usize") // To be SURE that the search will be run. press-key: 'Enter' // Waiting for the search results to appear... wait-for: "#search-tabs" +wait-for-false: "#search-tabs .count.loading" // Check the disambiguated link. assert-count: ("a.result-method", 1) assert-attribute: ("a.result-method", { @@ -47,6 +53,7 @@ assert: "section:target" // impl block's disambiguator is also acted upon. go-to: "file://" + |DOC_PATH| + "/lib2/index.html?search=MultiImplBlockStruct->bool" wait-for: "#search-tabs" +wait-for-false: "#search-tabs .count.loading" assert-count: ("a.result-method", 1) assert-attribute: ("a.result-method", { "href": "../lib2/another_mod/struct.MultiImplBlockStruct.html#impl-MultiImplBlockStruct/method.second_fn" @@ -56,6 +63,7 @@ wait-for: "details:has(summary > #impl-MultiImplBlockStruct-1) > div section[id= go-to: "file://" + |DOC_PATH| + "/lib2/index.html?search=MultiImplBlockStruct->u32" wait-for: "#search-tabs" +wait-for-false: "#search-tabs .count.loading" assert-count: ("a.result-method", 1) assert-attribute: ("a.result-method", { "href": "../lib2/another_mod/struct.MultiImplBlockStruct.html#impl-MultiImplBlockTrait-for-MultiImplBlockStruct/method.second_fn" diff --git a/tests/rustdoc-gui/search-result-keyword.goml b/tests/rustdoc-gui/search-result-keyword.goml index 02305f2587c9c..0474e8b5664bb 100644 --- a/tests/rustdoc-gui/search-result-keyword.goml +++ b/tests/rustdoc-gui/search-result-keyword.goml @@ -1,8 +1,10 @@ // Checks that the "keyword" results have the expected text alongside them. go-to: "file://" + |DOC_PATH| + "/test_docs/index.html" +click: "#search-button" write-into: (".search-input", "for") // To be SURE that the search will be run. press-key: 'Enter' // Waiting for the search results to appear... wait-for: "#search-tabs" +wait-for-false: "#search-tabs .count.loading" assert-text: (".result-keyword .result-name", "keyword for") diff --git a/tests/rustdoc-gui/search-tab-change-title-fn-sig.goml b/tests/rustdoc-gui/search-tab-change-title-fn-sig.goml index 7e26229ec6e51..e7145176de74d 100644 --- a/tests/rustdoc-gui/search-tab-change-title-fn-sig.goml +++ b/tests/rustdoc-gui/search-tab-change-title-fn-sig.goml @@ -1,11 +1,14 @@ // Checks that the search tab results work correctly with function signature syntax // First, try a search-by-name go-to: "file://" + |DOC_PATH| + "/test_docs/index.html" +click: "#search-button" +wait-for: ".search-input" write-into: (".search-input", "Foo") // To be SURE that the search will be run. press-key: 'Enter' // Waiting for the search results to appear... wait-for: "#search-tabs" +wait-for-false: "#search-tabs .count.loading" assert-attribute: ("#search-tabs > button:nth-of-type(1)", {"class": "selected"}) assert-text: ("#search-tabs > button:nth-of-type(1)", "In Names", STARTS_WITH) assert: "input.search-input:focus" @@ -23,11 +26,14 @@ wait-for-attribute: ("#search-tabs > button:nth-of-type(3)", {"class": "selected // Now try search-by-return go-to: "file://" + |DOC_PATH| + "/test_docs/index.html" +click: "#search-button" +wait-for: ".search-input" write-into: (".search-input", "-> String") // To be SURE that the search will be run. press-key: 'Enter' // Waiting for the search results to appear... wait-for: "#search-tabs" +wait-for-false: "#search-tabs .count.loading" assert-attribute: ("#search-tabs > button:nth-of-type(1)", {"class": "selected"}) assert-text: ("#search-tabs > button:nth-of-type(1)", "In Function Return Types", STARTS_WITH) assert: "input.search-input:focus" @@ -45,11 +51,13 @@ wait-for-attribute: ("#search-tabs > button:nth-of-type(1)", {"class": "selected // Try with a search-by-return with no results go-to: "file://" + |DOC_PATH| + "/test_docs/index.html" +click: "#search-button" write-into: (".search-input", "-> Something") // To be SURE that the search will be run. press-key: 'Enter' // Waiting for the search results to appear... wait-for: "#search-tabs" +wait-for-false: "#search-tabs .count.loading" assert-attribute: ("#search-tabs > button:nth-of-type(1)", {"class": "selected"}) assert-text: ("#search-tabs > button:nth-of-type(1)", "In Function Return Types", STARTS_WITH) @@ -60,15 +68,18 @@ write-into: (".search-input", "usize,pattern") press-key: 'Enter' // Waiting for the search results to appear... wait-for: "#search-tabs" +wait-for-false: "#search-tabs .count.loading" assert-attribute: ("#search-tabs > button:nth-of-type(1)", {"class": "selected"}) assert-text: ("#search-tabs > button:nth-of-type(1)", "In Function Parameters", STARTS_WITH) // Try with a search-by-parameter-and-return go-to: "file://" + |DOC_PATH| + "/test_docs/index.html" +click: "#search-button" write-into: (".search-input", "pattern -> str") // To be SURE that the search will be run. press-key: 'Enter' // Waiting for the search results to appear... wait-for: "#search-tabs" +wait-for-false: "#search-tabs .count.loading" assert-attribute: ("#search-tabs > button:nth-of-type(1)", {"class": "selected"}) assert-text: ("#search-tabs > button:nth-of-type(1)", "In Function Signatures", STARTS_WITH) diff --git a/tests/rustdoc-gui/search-tab.goml b/tests/rustdoc-gui/search-tab.goml index 826e272e508e0..00ca952033dc6 100644 --- a/tests/rustdoc-gui/search-tab.goml +++ b/tests/rustdoc-gui/search-tab.goml @@ -15,7 +15,8 @@ define-function: ( focus: ".search-input" press-key: "Enter" - wait-for: "#search-tabs" + wait-for: "#search-tabs .count" + wait-for-false: "#search-tabs .count.loading" assert-css: ("#search-tabs > button:not(.selected)", { "background-color": |background|, "border-bottom": |border_bottom|, diff --git a/tests/rustdoc-gui/search-title.goml b/tests/rustdoc-gui/search-title.goml index 95bc36af44911..c748a2046a4bb 100644 --- a/tests/rustdoc-gui/search-title.goml +++ b/tests/rustdoc-gui/search-title.goml @@ -5,10 +5,13 @@ go-to: "file://" + |DOC_PATH| + "/test_docs/index.html" store-value: (title, "test_docs - Rust") assert-document-property: {"title": |title|} +click: "#search-button" +wait-for: ".search-input" write-into: (".search-input", "test") // To be SURE that the search will be run. press-key: 'Enter' wait-for: "#crate-search" +wait-for-false: "#search-tabs .count.loading" assert-document-property: {"title": '"test" Search - Rust'} @@ -16,6 +19,7 @@ set-property: (".search-input", {"value": "another one"}) // To be SURE that the search will be run. press-key: 'Enter' wait-for: "#crate-search" +wait-for-false: "#search-tabs .count.loading" assert-document-property: {"title": '"another one" Search - Rust'} diff --git a/tests/rustdoc-gui/setting-auto-hide-content-large-items.goml b/tests/rustdoc-gui/setting-auto-hide-content-large-items.goml index 9afde7c61da69..342bd726694e7 100644 --- a/tests/rustdoc-gui/setting-auto-hide-content-large-items.goml +++ b/tests/rustdoc-gui/setting-auto-hide-content-large-items.goml @@ -9,7 +9,7 @@ define-function: ( [storage_value, setting_attribute_value, toggle_attribute_value], block { assert-local-storage: {"rustdoc-auto-hide-large-items": |storage_value|} - click: "#settings-menu" + click: "rustdoc-toolbar .settings-menu" wait-for: "#settings" assert-property: ("#auto-hide-large-items", {"checked": |setting_attribute_value|}) assert-attribute: (".item-decl .type-contents-toggle", {"open": |toggle_attribute_value|}) diff --git a/tests/rustdoc-gui/setting-auto-hide-item-methods-docs.goml b/tests/rustdoc-gui/setting-auto-hide-item-methods-docs.goml index 644396ed57826..02d4ce8855fdc 100644 --- a/tests/rustdoc-gui/setting-auto-hide-item-methods-docs.goml +++ b/tests/rustdoc-gui/setting-auto-hide-item-methods-docs.goml @@ -6,7 +6,7 @@ define-function: ( [storage_value, setting_attribute_value, toggle_attribute_value], block { assert-local-storage: {"rustdoc-auto-hide-method-docs": |storage_value|} - click: "#settings-menu" + click: "rustdoc-toolbar .settings-menu" wait-for: "#settings" assert-property: ("#auto-hide-method-docs", {"checked": |setting_attribute_value|}) assert-attribute: (".toggle.method-toggle", {"open": |toggle_attribute_value|}) diff --git a/tests/rustdoc-gui/setting-auto-hide-trait-implementations.goml b/tests/rustdoc-gui/setting-auto-hide-trait-implementations.goml index 3c09198dae509..4af1e829b31c7 100644 --- a/tests/rustdoc-gui/setting-auto-hide-trait-implementations.goml +++ b/tests/rustdoc-gui/setting-auto-hide-trait-implementations.goml @@ -5,7 +5,7 @@ define-function: ( [storage_value, setting_attribute_value, toggle_attribute_value], block { assert-local-storage: {"rustdoc-auto-hide-trait-implementations": |storage_value|} - click: "#settings-menu" + click: "rustdoc-toolbar .settings-menu" wait-for: "#settings" assert-property: ("#auto-hide-trait-implementations", {"checked": |setting_attribute_value|}) assert-attribute: ("#trait-implementations-list > details", {"open": |toggle_attribute_value|}, ALL) diff --git a/tests/rustdoc-gui/setting-go-to-only-result.goml b/tests/rustdoc-gui/setting-go-to-only-result.goml index f8535477c22d0..5a9c81e0b836c 100644 --- a/tests/rustdoc-gui/setting-go-to-only-result.goml +++ b/tests/rustdoc-gui/setting-go-to-only-result.goml @@ -5,7 +5,7 @@ define-function: ( [storage_value, setting_attribute_value], block { assert-local-storage: {"rustdoc-go-to-only-result": |storage_value|} - click: "#settings-menu" + click: "rustdoc-toolbar .settings-menu" wait-for: "#settings" assert-property: ("#go-to-only-result", {"checked": |setting_attribute_value|}) } @@ -25,7 +25,7 @@ wait-for: "#search" assert-document-property: ({"URL": "/lib2/index.html"}, CONTAINS) // Now we change its value. -click: "#settings-menu" +click: "rustdoc-toolbar .settings-menu" wait-for: "#settings" click: "#go-to-only-result" assert-local-storage: {"rustdoc-go-to-only-result": "true"} diff --git a/tests/rustdoc-gui/settings-button.goml b/tests/rustdoc-gui/settings-button.goml index d78034769e245..28ce06207aa41 100644 --- a/tests/rustdoc-gui/settings-button.goml +++ b/tests/rustdoc-gui/settings-button.goml @@ -9,7 +9,7 @@ define-function: ( [theme, filter], block { call-function: ("switch-theme", {"theme": |theme|}) - assert-css: ("#settings-menu > a::before", { + assert-css: ("rustdoc-toolbar .settings-menu > a::before", { "filter": |filter|, "width": "18px", "height": "18px", diff --git a/tests/rustdoc-gui/settings.goml b/tests/rustdoc-gui/settings.goml index 11d3696ccf6a5..7a163103b5ab1 100644 --- a/tests/rustdoc-gui/settings.goml +++ b/tests/rustdoc-gui/settings.goml @@ -5,7 +5,7 @@ show-text: true // needed when we check for colors below. // First, we check that the settings page doesn't exist. assert-false: "#settings" // We now click on the settings button. -click: "#settings-menu" +click: "rustdoc-toolbar .settings-menu" wait-for: "#settings" assert-css: ("#settings", {"display": "block"}) @@ -13,11 +13,11 @@ assert-css: ("#settings", {"display": "block"}) store-css: (".setting-line", {"margin": setting_line_margin}) // Let's close it by clicking on the same button. -click: "#settings-menu" +click: "rustdoc-toolbar .settings-menu" wait-for-css: ("#settings", {"display": "none"}) // Let's check that pressing "ESCAPE" is closing it. -click: "#settings-menu" +click: "rustdoc-toolbar .settings-menu" wait-for-css: ("#settings", {"display": "block"}) press-key: "Escape" wait-for-css: ("#settings", {"display": "none"}) @@ -28,7 +28,7 @@ write: "test" // To be SURE that the search will be run. press-key: 'Enter' wait-for: "#alternative-display #search" -click: "#settings-menu" +click: "rustdoc-toolbar .settings-menu" wait-for-css: ("#settings", {"display": "block"}) // Ensure that the search is still displayed. wait-for: "#alternative-display #search" @@ -41,7 +41,7 @@ set-local-storage: {"rustdoc-theme": "dark", "rustdoc-use-system-theme": "false" // We reload the page so the local storage settings are being used. reload: -click: "#settings-menu" +click: "rustdoc-toolbar .settings-menu" wait-for: "#settings" // We check that the "Use system theme" is disabled. @@ -55,7 +55,7 @@ assert: "#preferred-light-theme.setting-line.hidden" assert-property: ("#theme .setting-radio-choices #theme-dark", {"checked": "true"}) // Some style checks... -move-cursor-to: "#settings-menu > a" +move-cursor-to: "rustdoc-toolbar .settings-menu > a" // First we check the "default" display for radio buttons. assert-css: ( "#theme-dark", @@ -194,7 +194,7 @@ assert-css: ( "border-width": "2px", }, ) -move-cursor-to: "#settings-menu > a" +move-cursor-to: "rustdoc-toolbar .settings-menu > a" // Let's now check with the focus for toggles. focus: "#auto-hide-large-items" assert-css: ( @@ -273,43 +273,43 @@ assert-local-storage: {"rustdoc-disable-shortcuts": "true"} press-key: "Escape" press-key: "?" assert-false: "#help-button .popover" -wait-for-css: ("#settings-menu .popover", {"display": "block"}) +wait-for-css: ("rustdoc-toolbar .settings-menu .popover", {"display": "block"}) // Now turn keyboard shortcuts back on, and see if they work. click: "#disable-shortcuts" assert-local-storage: {"rustdoc-disable-shortcuts": "false"} press-key: "Escape" press-key: "?" -wait-for-css: ("#help-button .popover", {"display": "block"}) -assert-css: ("#settings-menu .popover", {"display": "none"}) +wait-for-css: ("rustdoc-toolbar .help-menu .popover", {"display": "block"}) +assert-css: ("rustdoc-toolbar .settings-menu .popover", {"display": "none"}) // Now switch back to the settings popover, and make sure the keyboard // shortcut works when a check box is selected. -click: "#settings-menu > a" -wait-for-css: ("#settings-menu .popover", {"display": "block"}) +click: "rustdoc-toolbar .settings-menu > a" +wait-for-css: ("rustdoc-toolbar .settings-menu .popover", {"display": "block"}) focus: "#auto-hide-large-items" press-key: "?" -wait-for-css: ("#settings-menu .popover", {"display": "none"}) -wait-for-css: ("#help-button .popover", {"display": "block"}) +wait-for-css: ("rustdoc-toolbar .settings-menu .popover", {"display": "none"}) +wait-for-css: ("rustdoc-toolbar .help-menu .popover", {"display": "block"}) // Now switch back to the settings popover, and make sure the keyboard // shortcut works when a check box is selected. -click: "#settings-menu > a" -wait-for-css: ("#settings-menu .popover", {"display": "block"}) -wait-for-css: ("#help-button .popover", {"display": "none"}) +click: "rustdoc-toolbar .settings-menu > a" +wait-for-css: ("rustdoc-toolbar .settings-menu .popover", {"display": "block"}) +assert-false: "rustdoc-toolbar .help-menu .popover" focus: "#theme-system-preference" press-key: "?" -wait-for-css: ("#settings-menu .popover", {"display": "none"}) -wait-for-css: ("#help-button .popover", {"display": "block"}) +wait-for-css: ("rustdoc-toolbar .settings-menu .popover", {"display": "none"}) +wait-for-css: ("rustdoc-toolbar .help-menu .popover", {"display": "block"}) // Now we go to the settings page to check that the CSS is loaded as expected. go-to: "file://" + |DOC_PATH| + "/settings.html" wait-for: "#settings" -assert-false: "#settings-menu" +assert-false: "rustdoc-toolbar .settings-menu" assert-css: (".setting-radio", {"cursor": "pointer"}) assert-attribute-false: ("#settings", {"class": "popover"}, CONTAINS) -compare-elements-position: (".sub form", "#settings", ["x"]) +compare-elements-position: (".main-heading", "#settings", ["x"]) // Check that setting-line has the same margin in this mode as in the popover. assert-css: (".setting-line", {"margin": |setting_line_margin|}) diff --git a/tests/rustdoc-gui/shortcuts.goml b/tests/rustdoc-gui/shortcuts.goml index 5a6171d6f761a..b27cf8c407d8c 100644 --- a/tests/rustdoc-gui/shortcuts.goml +++ b/tests/rustdoc-gui/shortcuts.goml @@ -8,9 +8,9 @@ press-key: "Escape" assert-false: "input.search-input:focus" // We now check for the help popup. press-key: "?" -assert-css: ("#help-button .popover", {"display": "block"}) +assert-css: ("rustdoc-toolbar .help-menu .popover", {"display": "block"}) press-key: "Escape" -assert-css: ("#help-button .popover", {"display": "none"}) +assert-false: "rustdoc-toolbar .help-menu .popover" // Checking doc collapse and expand. // It should be displaying a "-": assert-text: ("#toggle-all-docs", "Summary") diff --git a/tests/rustdoc-gui/sidebar-mobile.goml b/tests/rustdoc-gui/sidebar-mobile.goml index 6ddc07c6481c1..f828516d76246 100644 --- a/tests/rustdoc-gui/sidebar-mobile.goml +++ b/tests/rustdoc-gui/sidebar-mobile.goml @@ -17,7 +17,7 @@ assert-css: (".sidebar", {"display": "block", "left": "-1000px"}) focus: ".sidebar-elems h3 a" assert-css: (".sidebar", {"display": "block", "left": "0px"}) // When we tab out of the sidebar, close it. -focus: ".search-input" +focus: "#search-button" assert-css: (".sidebar", {"display": "block", "left": "-1000px"}) // Open the sidebar menu. @@ -43,7 +43,7 @@ press-key: "Escape" assert-css: (".sidebar", {"display": "block", "left": "-1000px"}) // Check that the topbar is visible -assert-property: (".mobile-topbar", {"clientHeight": "45"}) +assert-property: ("rustdoc-topbar", {"clientHeight": "45"}) // Check that clicking an element from the sidebar scrolls to the right place // so the target is not obscured by the topbar. @@ -54,7 +54,7 @@ assert-position: ("#method\.must_use", {"y": 46}) // Check that the bottom-most item on the sidebar menu can be scrolled fully into view. click: ".sidebar-menu-toggle" scroll-to: ".block.keyword li:nth-child(1)" -compare-elements-position-near: (".block.keyword li:nth-child(1)", ".mobile-topbar", {"y": 544}) +compare-elements-position-near: (".block.keyword li:nth-child(1)", "rustdoc-topbar", {"y": 544}) // Now checking the background color of the sidebar. // Close the sidebar menu. @@ -65,7 +65,7 @@ define-function: ( "check-colors", [theme, color, background], block { - call-function: ("switch-theme", {"theme": |theme|}) + call-function: ("switch-theme-mobile", {"theme": |theme|}) reload: // Open the sidebar menu. diff --git a/tests/rustdoc-gui/sidebar-resize-close-popover.goml b/tests/rustdoc-gui/sidebar-resize-close-popover.goml index 2d26caf1a39ef..d3fea9b0f4003 100644 --- a/tests/rustdoc-gui/sidebar-resize-close-popover.goml +++ b/tests/rustdoc-gui/sidebar-resize-close-popover.goml @@ -2,7 +2,7 @@ go-to: "file://" + |DOC_PATH| + "/test_docs/index.html" assert-property: (".sidebar", {"clientWidth": "199"}) show-text: true -click: "#settings-menu" +click: "rustdoc-toolbar .settings-menu" wait-for: "#settings" assert-css: ("#settings", {"display": "block"}) // normal resizing @@ -12,7 +12,7 @@ assert-css: ("#settings", {"display": "none"}) // Now same thing, but for source code go-to: "file://" + |DOC_PATH| + "/src/test_docs/lib.rs.html" -click: "#settings-menu" +click: "rustdoc-toolbar .settings-menu" wait-for: "#settings" assert-css: ("#settings", {"display": "block"}) assert-property: (".sidebar", {"clientWidth": "49"}) diff --git a/tests/rustdoc-gui/sidebar-resize-setting.goml b/tests/rustdoc-gui/sidebar-resize-setting.goml index e346fe6aeac0b..a4572c670f81a 100644 --- a/tests/rustdoc-gui/sidebar-resize-setting.goml +++ b/tests/rustdoc-gui/sidebar-resize-setting.goml @@ -4,7 +4,7 @@ assert-property: (".sidebar", {"clientWidth": "199"}) show-text: true // Verify that the "hide" option is unchecked -click: "#settings-menu" +click: "rustdoc-toolbar .settings-menu" wait-for: "#settings" assert-css: ("#settings", {"display": "block"}) assert-property: ("#hide-sidebar", {"checked": "false"}) @@ -15,7 +15,7 @@ drag-and-drop: ((205, 100), (5, 100)) assert-css: (".sidebar", {"display": "none"}) // Verify that the "hide" option is checked -focus: "#settings-menu a" +focus: "rustdoc-toolbar .settings-menu a" press-key: "Enter" wait-for-css: ("#settings", {"display": "block"}) assert-property: ("#hide-sidebar", {"checked": "true"}) @@ -24,28 +24,28 @@ wait-for-css: (".sidebar", {"display": "block"}) // Verify that hiding the sidebar hides the source sidebar // and puts the button in static position mode on mobile -go-to: "file://" + |DOC_PATH| + "/src/test_docs/lib.rs.html" +go-to: "file://" + |DOC_PATH| + "/test_docs/index.html" set-window-size: (600, 600) -focus: "#settings-menu a" +focus: "rustdoc-topbar .settings-menu a" press-key: "Enter" wait-for-css: ("#settings", {"display": "block"}) +wait-for-css: ("#sidebar-button", {"position": "static"}) +assert-property: ("#hide-sidebar", {"checked": "false"}) +click: "#hide-sidebar" +wait-for-css: (".sidebar", {"display": "none"}) wait-for-css: ("#sidebar-button", {"position": "fixed"}) store-position: ("#sidebar-button", { "y": sidebar_button_y, "x": sidebar_button_x, }) -assert-property: ("#hide-sidebar", {"checked": "false"}) -click: "#hide-sidebar" -wait-for-css: (".sidebar", {"display": "none"}) -wait-for-css: ("#sidebar-button", {"position": "static"}) -assert-position: ("#sidebar-button", { - "y": |sidebar_button_y|, - "x": |sidebar_button_x|, -}) assert-property: ("#hide-sidebar", {"checked": "true"}) press-key: "Escape" // Clicking the sidebar button should work, and implicitly re-enable // the persistent navigation bar wait-for-css: ("#settings", {"display": "none"}) +assert-position: ("#sidebar-button", { + "y": |sidebar_button_y|, + "x": |sidebar_button_x|, +}) click: "#sidebar-button" wait-for-css: (".sidebar", {"display": "block"}) diff --git a/tests/rustdoc-gui/sidebar-source-code-display.goml b/tests/rustdoc-gui/sidebar-source-code-display.goml index 1e77bcc22731e..1e7d61151a566 100644 --- a/tests/rustdoc-gui/sidebar-source-code-display.goml +++ b/tests/rustdoc-gui/sidebar-source-code-display.goml @@ -141,7 +141,7 @@ click: "#sidebar-button" wait-for-css: (".src .sidebar > *", {"visibility": "hidden"}) // We scroll to line 117 to change the scroll position. scroll-to: '//*[@id="117"]' -store-value: (y_offset, "2578") +store-value: (y_offset, "2549") assert-window-property: {"pageYOffset": |y_offset|} // Expanding the sidebar... click: "#sidebar-button" diff --git a/tests/rustdoc-gui/sidebar-source-code.goml b/tests/rustdoc-gui/sidebar-source-code.goml index 6afccf6a95fe2..0ac88612cefe9 100644 --- a/tests/rustdoc-gui/sidebar-source-code.goml +++ b/tests/rustdoc-gui/sidebar-source-code.goml @@ -85,4 +85,4 @@ assert-false: ".src-sidebar-expanded" assert: "nav.sidebar" // Check that the topbar is not visible -assert-false: ".mobile-topbar" +assert-false: "rustdoc-topbar" diff --git a/tests/rustdoc-gui/sidebar.goml b/tests/rustdoc-gui/sidebar.goml index c0fe240e2bed2..f437ff81459d7 100644 --- a/tests/rustdoc-gui/sidebar.goml +++ b/tests/rustdoc-gui/sidebar.goml @@ -200,7 +200,7 @@ drag-and-drop: ((205, 100), (108, 100)) assert-position: (".sidebar-crate > h2 > a", {"x": -3}) // Check that the mobile sidebar and the source sidebar use the same icon. -store-css: (".mobile-topbar .sidebar-menu-toggle::before", {"content": image_url}) +store-css: ("rustdoc-topbar .sidebar-menu-toggle::before", {"content": image_url}) // Then we go to a source page. click: ".main-heading .src" assert-css: ("#sidebar-button a::before", {"content": |image_url|}) @@ -212,7 +212,7 @@ assert: |sidebar_background| != |sidebar_background_hover| click: "#sidebar-button a" wait-for: "html.src-sidebar-expanded" assert-css: ("#sidebar-button a:hover", {"background-color": |sidebar_background_hover|}) -move-cursor-to: "#settings-menu" +move-cursor-to: "#search-button" assert-css: ("#sidebar-button a:not(:hover)", {"background-color": |sidebar_background|}) // Closing sidebar. click: "#sidebar-button a" diff --git a/tests/rustdoc-gui/source-anchor-scroll.goml b/tests/rustdoc-gui/source-anchor-scroll.goml index c005af1e7a1c6..09b8f3a4271c1 100644 --- a/tests/rustdoc-gui/source-anchor-scroll.goml +++ b/tests/rustdoc-gui/source-anchor-scroll.goml @@ -8,13 +8,13 @@ set-window-size: (600, 800) assert-property: ("html", {"scrollTop": "0"}) click: '//a[text() = "barbar" and @href="#5-7"]' -assert-property: ("html", {"scrollTop": "206"}) +assert-property: ("html", {"scrollTop": "177"}) click: '//a[text() = "bar" and @href="#28-36"]' -assert-property: ("html", {"scrollTop": "239"}) +assert-property: ("html", {"scrollTop": "210"}) click: '//a[normalize-space() = "sub_fn" and @href="#2-4"]' -assert-property: ("html", {"scrollTop": "134"}) +assert-property: ("html", {"scrollTop": "105"}) // We now check that clicking on lines doesn't change the scroll // Extra information: the "sub_fn" function header is on line 1. click: '//*[@id="6"]' -assert-property: ("html", {"scrollTop": "134"}) +assert-property: ("html", {"scrollTop": "105"}) diff --git a/tests/rustdoc-gui/source-code-page.goml b/tests/rustdoc-gui/source-code-page.goml index aa5a16aac7048..5e3470dca20ea 100644 --- a/tests/rustdoc-gui/source-code-page.goml +++ b/tests/rustdoc-gui/source-code-page.goml @@ -89,9 +89,9 @@ assert-css: ("a[data-nosnippet]", {"text-align": "right"}, ALL) // do anything (and certainly not add a `#NaN` to the URL!). go-to: "file://" + |DOC_PATH| + "/src/test_docs/lib.rs.html" // We use this assert-position to know where we will click. -assert-position: ("//*[@id='1']", {"x": 81, "y": 169}) -// We click on the left of the "1" anchor but still in the `a[data-nosnippet]`. -click: (77, 163) +assert-position: ("//*[@id='1']", {"x": 81, "y": 141}) +// We click on the left of the "1" anchor but still in the "src-line-number" `<pre>`. +click: (135, 77) assert-document-property: ({"URL": "/lib.rs.html"}, ENDS_WITH) // Checking the source code sidebar. @@ -156,27 +156,8 @@ call-function: ("check-sidebar-dir-entry", { "y": |source_sidebar_title_y| + |source_sidebar_title_height| + 7, }) -// Check the search form -assert-css: ("nav.sub", {"flex-direction": "row"}) -// The goal of this test is to ensure the search input is perfectly centered -// between the top of the page and the header. -// To check this, we maintain the invariant: -// -// offsetTop[nav.sub form] = offsetTop[#main-content] - offsetHeight[nav.sub form] - offsetTop[nav.sub form] -assert-position: ("nav.sub form", {"y": 15}) -assert-property: ("nav.sub form", {"offsetHeight": 34}) -assert-position: ("h1", {"y": 68}) -// 15 = 64 - 34 - 15 - -// Now do the same check on moderately-sized, tablet mobile. -set-window-size: (700, 700) -assert-css: ("nav.sub", {"flex-direction": "row"}) -assert-position: ("nav.sub form", {"y": 8}) -assert-property: ("nav.sub form", {"offsetHeight": 34}) -assert-position: ("h1", {"y": 54}) -// 8 = 50 - 34 - 8 - // Check the sidebar directory entries have a marker and spacing (tablet). +set-window-size: (700, 700) store-property: (".src-sidebar-title", { "offsetHeight": source_sidebar_title_height, "offsetTop": source_sidebar_title_y, @@ -187,11 +168,8 @@ call-function: ("check-sidebar-dir-entry", { "y": |source_sidebar_title_y| + |source_sidebar_title_height| + 7, }) -// Tiny, phone mobile gets a different display where the logo is stacked on top. -set-window-size: (450, 700) -assert-css: ("nav.sub", {"flex-direction": "column"}) - // Check the sidebar directory entries have a marker and spacing (phone). +set-window-size: (450, 700) store-property: (".src-sidebar-title", { "offsetHeight": source_sidebar_title_height, "offsetTop": source_sidebar_title_y, diff --git a/tests/rustdoc-gui/source-code-wrapping.goml b/tests/rustdoc-gui/source-code-wrapping.goml index 0dab9c72ea9fd..c1fc2835c89ae 100644 --- a/tests/rustdoc-gui/source-code-wrapping.goml +++ b/tests/rustdoc-gui/source-code-wrapping.goml @@ -13,7 +13,7 @@ define-function: ( ) store-size: (".rust code", {"width": width, "height": height}) -click: "#settings-menu" +click: "main .settings-menu" wait-for: "#settings" call-function: ("click-code-wrapping", {"expected": "true"}) wait-for-size-false: (".rust code", {"width": |width|, "height": |height|}) @@ -28,7 +28,7 @@ assert-size: (".rust code", {"width": |width|, "height": |height|}) // Now let's check in docs code examples. go-to: "file://" + |DOC_PATH| + "/test_docs/trait_bounds/index.html" -click: "#settings-menu" +click: "main .settings-menu" wait-for: "#settings" store-property: (".example-wrap .rust code", {"scrollWidth": rust_width, "scrollHeight": rust_height}) diff --git a/tests/rustdoc-gui/theme-change.goml b/tests/rustdoc-gui/theme-change.goml index 5898711050992..3860596e34330 100644 --- a/tests/rustdoc-gui/theme-change.goml +++ b/tests/rustdoc-gui/theme-change.goml @@ -7,7 +7,7 @@ store-value: (background_light, "white") store-value: (background_dark, "#353535") store-value: (background_ayu, "#0f1419") -click: "#settings-menu" +click: "rustdoc-toolbar .settings-menu" wait-for: "#theme-ayu" click: "#theme-ayu" // should be the ayu theme so let's check the color. @@ -75,7 +75,7 @@ store-value: (background_dark, "#353535") store-value: (background_ayu, "#0f1419") store-value: (background_custom_theme, "red") -click: "#settings-menu" +click: "rustdoc-toolbar .settings-menu" wait-for: "#theme-ayu" click: "#theme-ayu" // should be the ayu theme so let's check the color. diff --git a/tests/rustdoc-gui/theme-defaults.goml b/tests/rustdoc-gui/theme-defaults.goml index 2cc5d716cfefe..12c17166e8744 100644 --- a/tests/rustdoc-gui/theme-defaults.goml +++ b/tests/rustdoc-gui/theme-defaults.goml @@ -1,6 +1,6 @@ // Ensure that the theme picker always starts with the actual defaults. go-to: "file://" + |DOC_PATH| + "/test_docs/index.html" -click: "#settings-menu" +click: "rustdoc-toolbar .settings-menu" wait-for: "#theme-system-preference" assert: "#theme-system-preference:checked" assert: "#preferred-light-theme-light:checked" @@ -16,7 +16,7 @@ set-local-storage: { "rustdoc-theme": "ayu" } go-to: "file://" + |DOC_PATH| + "/test_docs/index.html" -click: "#settings-menu" +click: "rustdoc-toolbar .settings-menu" wait-for: "#theme-system-preference" assert: "#theme-system-preference:checked" assert: "#preferred-light-theme-light:checked" diff --git a/tests/rustdoc-gui/toggle-click-deadspace.goml b/tests/rustdoc-gui/toggle-click-deadspace.goml index caca1b61493d2..c6d1397308785 100644 --- a/tests/rustdoc-gui/toggle-click-deadspace.goml +++ b/tests/rustdoc-gui/toggle-click-deadspace.goml @@ -13,4 +13,4 @@ assert-attribute-false: (".impl-items .toggle", {"open": ""}) // Click the "Trait" part of "impl Trait" and verify it navigates. click: "#impl-Trait-for-Foo h3 a:first-of-type" assert-text: (".main-heading .rustdoc-breadcrumbs", "lib2") -assert-text: (".main-heading h1", "Trait TraitCopy item path") +assert-text: (".main-heading h1", "Trait Trait Copy item path") diff --git a/tests/rustdoc-gui/toggle-docs-mobile-failure.png b/tests/rustdoc-gui/toggle-docs-mobile-failure.png new file mode 100644 index 0000000000000..7a5db4673f406 Binary files /dev/null and b/tests/rustdoc-gui/toggle-docs-mobile-failure.png differ diff --git a/tests/rustdoc-gui/toggle-docs-mobile.goml b/tests/rustdoc-gui/toggle-docs-mobile.goml index 6a40ba83b843e..d038ebcdd6f8d 100644 --- a/tests/rustdoc-gui/toggle-docs-mobile.goml +++ b/tests/rustdoc-gui/toggle-docs-mobile.goml @@ -3,12 +3,12 @@ go-to: "file://" + |DOC_PATH| + "/test_docs/struct.Foo.html" set-window-size: (433, 600) assert-attribute: (".top-doc", {"open": ""}) -click: (4, 270) // This is the position of the top doc comment toggle +click: (4, 230) // This is the position of the top doc comment toggle assert-attribute-false: (".top-doc", {"open": ""}) -click: (4, 270) +click: (4, 230) assert-attribute: (".top-doc", {"open": ""}) // To ensure that the toggle isn't over the text, we check that the toggle isn't clicked. -click: (3, 270) +click: (3, 230) assert-attribute: (".top-doc", {"open": ""}) // Assert the position of the toggle on the top doc block. @@ -24,12 +24,12 @@ assert-position: ( // Now we do the same but with a little bigger width set-window-size: (600, 600) assert-attribute: (".top-doc", {"open": ""}) -click: (4, 270) // New Y position since all search elements are back on one line. +click: (4, 230) // New Y position since all search elements are back on one line. assert-attribute-false: (".top-doc", {"open": ""}) -click: (4, 270) +click: (4, 230) assert-attribute: (".top-doc", {"open": ""}) // To ensure that the toggle isn't over the text, we check that the toggle isn't clicked. -click: (3, 270) +click: (3, 230) assert-attribute: (".top-doc", {"open": ""}) // Same check on trait items. diff --git a/tests/rustdoc-gui/toggle-docs.goml b/tests/rustdoc-gui/toggle-docs.goml index 4607c604eeb3d..9eea687f74e2f 100644 --- a/tests/rustdoc-gui/toggle-docs.goml +++ b/tests/rustdoc-gui/toggle-docs.goml @@ -64,7 +64,7 @@ define-function: ( "filter": |filter|, }) // moving the cursor somewhere else to not mess with next function calls. - move-cursor-to: ".search-input" + move-cursor-to: "#search-button" }, ) diff --git a/tests/rustdoc-gui/type-declation-overflow.goml b/tests/rustdoc-gui/type-declation-overflow.goml index 4f8fe78ea4dc6..0a856b554a784 100644 --- a/tests/rustdoc-gui/type-declation-overflow.goml +++ b/tests/rustdoc-gui/type-declation-overflow.goml @@ -47,18 +47,18 @@ assert-property: ("pre.item-decl", {"scrollWidth": "950"}) set-window-size: (600, 600) go-to: "file://" + |DOC_PATH| + "/lib2/too_long/struct.SuperIncrediblyLongLongLongLongLongLongLongGigaGigaGigaMegaLongLongLongStructName.html" // It shouldn't have an overflow in the topbar either. -store-property: (".mobile-topbar", {"scrollWidth": scrollWidth}) -assert-property: (".mobile-topbar", {"clientWidth": |scrollWidth|}) -assert-css: (".mobile-topbar h2", {"overflow-x": "hidden"}) +store-property: ("rustdoc-topbar", {"scrollWidth": scrollWidth}) +assert-property: ("rustdoc-topbar", {"clientWidth": |scrollWidth|}, NEAR) +assert-css: ("rustdoc-topbar h2", {"overflow-x": "hidden"}) // Check that main heading and toolbar go side-by-side, both on desktop and on mobile. set-window-size: (1100, 800) go-to: "file://" + |DOC_PATH| + "/lib2/too_long/struct.SuperIncrediblyLongLongLongLongLongLongLongGigaGigaGigaMegaLongLongLongStructName.html" compare-elements-position: (".main-heading h1", ".main-heading rustdoc-toolbar", ["y"]) -compare-elements-position-near-false: (".main-heading h1", ".main-heading rustdoc-toolbar", {"x": 550}) +compare-elements-position-near-false: (".main-heading h1", ".main-heading rustdoc-toolbar", {"x": 300}) go-to: "file://" + |DOC_PATH| + "/lib2/index.html" compare-elements-position: (".main-heading h1", ".main-heading rustdoc-toolbar", ["y"]) -compare-elements-position-near-false: (".main-heading h1", ".main-heading rustdoc-toolbar", {"x": 550}) +compare-elements-position-near-false: (".main-heading h1", ".main-heading rustdoc-toolbar", {"x": 300}) // On mobile, they always wrap. set-window-size: (600, 600) diff --git a/tests/rustdoc-gui/utils.goml b/tests/rustdoc-gui/utils.goml index 844dc98a5374c..7dac176666e7f 100644 --- a/tests/rustdoc-gui/utils.goml +++ b/tests/rustdoc-gui/utils.goml @@ -5,13 +5,31 @@ define-function: ( block { // Set the theme. // Open the settings menu. - click: "#settings-menu" + click: "rustdoc-toolbar .settings-menu" // Wait for the popover to appear... wait-for: "#settings" // Change the setting. click: "#theme-"+ |theme| // Close the popover. - click: "#settings-menu" + click: "rustdoc-toolbar .settings-menu" + // Ensure that the local storage was correctly updated. + assert-local-storage: {"rustdoc-theme": |theme|} + }, +) + +define-function: ( + "switch-theme-mobile", + [theme], + block { + // Set the theme. + // Open the settings menu. + click: "rustdoc-topbar .settings-menu" + // Wait for the popover to appear... + wait-for: "#settings" + // Change the setting. + click: "#theme-"+ |theme| + // Close the popover. + click: "rustdoc-topbar .settings-menu" // Ensure that the local storage was correctly updated. assert-local-storage: {"rustdoc-theme": |theme|} }, diff --git a/tests/rustdoc-js-std/alias-1.js b/tests/rustdoc-js-std/alias-1.js index c31d1a3b1ad7d..295384170609c 100644 --- a/tests/rustdoc-js-std/alias-1.js +++ b/tests/rustdoc-js-std/alias-1.js @@ -1,6 +1,11 @@ const EXPECTED = { 'query': '&', 'others': [ + { + 'path': 'std::ops', + 'name': 'BitAnd', + 'desc': "The bitwise AND operator <code>&</code>.", + }, { 'path': 'std', 'name': 'reference', diff --git a/tests/rustdoc-js-std/alias-2.js b/tests/rustdoc-js-std/alias-2.js index 5735b573bcbda..9e97501e44324 100644 --- a/tests/rustdoc-js-std/alias-2.js +++ b/tests/rustdoc-js-std/alias-2.js @@ -1,9 +1,7 @@ const EXPECTED = { 'query': '+', 'others': [ - { 'path': 'std::ops', 'name': 'AddAssign' }, { 'path': 'std::ops', 'name': 'Add' }, - { 'path': 'core::ops', 'name': 'AddAssign' }, - { 'path': 'core::ops', 'name': 'Add' }, + { 'path': 'std::ops', 'name': 'AddAssign' }, ], }; diff --git a/tests/rustdoc-js-std/basic.js b/tests/rustdoc-js-std/basic.js index baff24b0af699..74467f0eef1d9 100644 --- a/tests/rustdoc-js-std/basic.js +++ b/tests/rustdoc-js-std/basic.js @@ -9,6 +9,6 @@ const EXPECTED = { { 'path': 'std::str', 'name': 'eq' }, ], 'returned': [ - { 'path': 'std::string::String', 'name': 'add' }, + { 'path': 'std::string::String', 'name': 'new' }, ], }; diff --git a/tests/rustdoc-js-std/parser-bindings.js b/tests/rustdoc-js-std/parser-bindings.js index bd379f139ffa2..e00e3088303d5 100644 --- a/tests/rustdoc-js-std/parser-bindings.js +++ b/tests/rustdoc-js-std/parser-bindings.js @@ -20,12 +20,12 @@ const PARSED = [ pathLast: "c", normalizedPathLast: "c", generics: [], - typeFilter: -1, + typeFilter: null, }, ] ], ], - typeFilter: -1, + typeFilter: null, }, ], foundElems: 1, @@ -51,11 +51,11 @@ const PARSED = [ pathWithoutLast: [], pathLast: "c", generics: [], - typeFilter: -1, + typeFilter: null, }] ], ], - typeFilter: -1, + typeFilter: null, }, ], foundElems: 1, @@ -81,11 +81,11 @@ const PARSED = [ pathWithoutLast: [], pathLast: "never", generics: [], - typeFilter: 1, + typeFilter: "primitive", }] ], ], - typeFilter: -1, + typeFilter: null, }, ], foundElems: 1, @@ -111,11 +111,11 @@ const PARSED = [ pathWithoutLast: [], pathLast: "[]", generics: [], - typeFilter: 1, + typeFilter: "primitive", }] ], ], - typeFilter: -1, + typeFilter: null, }, ], foundElems: 1, @@ -147,14 +147,14 @@ const PARSED = [ pathWithoutLast: [], pathLast: "never", generics: [], - typeFilter: 1, + typeFilter: "primitive", }, ], - typeFilter: 1, + typeFilter: "primitive", }] ], ], - typeFilter: -1, + typeFilter: null, }, ], foundElems: 1, @@ -213,7 +213,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "c", generics: [], - typeFilter: -1, + typeFilter: null, }, { name: "X", @@ -221,12 +221,12 @@ const PARSED = [ pathWithoutLast: [], pathLast: "x", generics: [], - typeFilter: -1, + typeFilter: null, }, ], ], ], - typeFilter: -1, + typeFilter: null, }, ], foundElems: 1, diff --git a/tests/rustdoc-js-std/parser-errors.js b/tests/rustdoc-js-std/parser-errors.js index 8bffef61c8f4f..49150cbd570ac 100644 --- a/tests/rustdoc-js-std/parser-errors.js +++ b/tests/rustdoc-js-std/parser-errors.js @@ -406,10 +406,10 @@ const PARSED = [ pathWithoutLast: [], pathLast: "x", generics: [], - typeFilter: -1, + typeFilter: null, }, ], - typeFilter: -1, + typeFilter: null, }, { name: "y", @@ -417,7 +417,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "y", generics: [], - typeFilter: -1, + typeFilter: null, }, ], foundElems: 2, @@ -440,7 +440,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "x", generics: [], - typeFilter: -1, + typeFilter: null, }, { name: "y", @@ -448,10 +448,10 @@ const PARSED = [ pathWithoutLast: [], pathLast: "y", generics: [], - typeFilter: -1, + typeFilter: null, }, ], - typeFilter: -1, + typeFilter: null, }, ], foundElems: 1, @@ -468,7 +468,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "p", generics: [], - typeFilter: -1, + typeFilter: null, }, { name: "x", @@ -476,7 +476,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "x", generics: [], - typeFilter: -1, + typeFilter: null, }, { name: "y", @@ -484,7 +484,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "y", generics: [], - typeFilter: -1, + typeFilter: null, }, ], foundElems: 3, diff --git a/tests/rustdoc-js-std/parser-filter.js b/tests/rustdoc-js-std/parser-filter.js index cda950461f7e0..569ef9aa96c4c 100644 --- a/tests/rustdoc-js-std/parser-filter.js +++ b/tests/rustdoc-js-std/parser-filter.js @@ -7,7 +7,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "foo", generics: [], - typeFilter: 7, + typeFilter: "fn", }], foundElems: 1, userQuery: "fn:foo", @@ -22,7 +22,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "foo", generics: [], - typeFilter: 6, + typeFilter: "enum", }], foundElems: 1, userQuery: "enum : foo", @@ -45,7 +45,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "macro", generics: [], - typeFilter: 16, + typeFilter: "macro", }], foundElems: 1, userQuery: "macro!", @@ -60,7 +60,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "mac", generics: [], - typeFilter: 16, + typeFilter: "macro", }], foundElems: 1, userQuery: "macro:mac!", @@ -75,7 +75,7 @@ const PARSED = [ pathWithoutLast: ["a"], pathLast: "mac", generics: [], - typeFilter: 16, + typeFilter: "macro", }], foundElems: 1, userQuery: "a::mac!", @@ -93,7 +93,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "foo", generics: [], - typeFilter: 7, + typeFilter: "fn", }], error: null, }, @@ -114,10 +114,10 @@ const PARSED = [ pathWithoutLast: [], pathLast: "bar", generics: [], - typeFilter: 7, + typeFilter: "fn", } ], - typeFilter: 7, + typeFilter: "fn", }], error: null, }, @@ -138,7 +138,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "bar", generics: [], - typeFilter: 7, + typeFilter: "fn", }, { name: "baz::fuzz", @@ -146,10 +146,10 @@ const PARSED = [ pathWithoutLast: ["baz"], pathLast: "fuzz", generics: [], - typeFilter: 6, + typeFilter: "enum", }, ], - typeFilter: 7, + typeFilter: "fn", }], error: null, }, diff --git a/tests/rustdoc-js-std/parser-generics.js b/tests/rustdoc-js-std/parser-generics.js index 8b8d95bcb8883..deaa0adbc63dc 100644 --- a/tests/rustdoc-js-std/parser-generics.js +++ b/tests/rustdoc-js-std/parser-generics.js @@ -16,7 +16,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "p", generics: [], - typeFilter: -1, + typeFilter: null, }, { name: "u8", @@ -24,7 +24,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "u8", generics: [], - typeFilter: -1, + typeFilter: null, }, ], foundElems: 2, @@ -49,7 +49,7 @@ const PARSED = [ generics: [], }, ], - typeFilter: -1, + typeFilter: null, }, ], foundElems: 1, @@ -82,7 +82,7 @@ const PARSED = [ ], }, ], - typeFilter: -1, + typeFilter: null, }, ], foundElems: 1, @@ -122,7 +122,7 @@ const PARSED = [ generics: [], }, ], - typeFilter: -1, + typeFilter: null, }, ], foundElems: 1, @@ -162,7 +162,7 @@ const PARSED = [ ], }, ], - typeFilter: -1, + typeFilter: null, }, ], foundElems: 1, diff --git a/tests/rustdoc-js-std/parser-hof.js b/tests/rustdoc-js-std/parser-hof.js index ca76101541280..5de232a66cdd4 100644 --- a/tests/rustdoc-js-std/parser-hof.js +++ b/tests/rustdoc-js-std/parser-hof.js @@ -25,11 +25,11 @@ const PARSED = [ generics: [], }, ], - typeFilter: -1, + typeFilter: null, }], ], ], - typeFilter: -1, + typeFilter: null, }], foundElems: 1, userQuery: "(-> F<P>)", @@ -53,11 +53,11 @@ const PARSED = [ pathWithoutLast: [], pathLast: "p", generics: [], - typeFilter: -1, + typeFilter: null, }], ], ], - typeFilter: -1, + typeFilter: null, }], foundElems: 1, userQuery: "(-> P)", @@ -81,11 +81,11 @@ const PARSED = [ pathWithoutLast: [], pathLast: "a", generics: [], - typeFilter: -1, + typeFilter: null, }], ], ], - typeFilter: -1, + typeFilter: null, }], foundElems: 1, userQuery: "(->,a)", @@ -113,7 +113,7 @@ const PARSED = [ generics: [], }, ], - typeFilter: -1, + typeFilter: null, }], bindings: [ [ @@ -121,7 +121,7 @@ const PARSED = [ [], ], ], - typeFilter: -1, + typeFilter: null, }], foundElems: 1, userQuery: "(F<P> ->)", @@ -141,7 +141,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "p", generics: [], - typeFilter: -1, + typeFilter: null, }], bindings: [ [ @@ -149,7 +149,7 @@ const PARSED = [ [], ], ], - typeFilter: -1, + typeFilter: null, }], foundElems: 1, userQuery: "(P ->)", @@ -169,7 +169,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "a", generics: [], - typeFilter: -1, + typeFilter: null, }], bindings: [ [ @@ -177,7 +177,7 @@ const PARSED = [ [], ], ], - typeFilter: -1, + typeFilter: null, }], foundElems: 1, userQuery: "(,a->)", @@ -197,7 +197,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "aaaaa", generics: [], - typeFilter: -1, + typeFilter: null, }], bindings: [ [ @@ -208,11 +208,11 @@ const PARSED = [ pathWithoutLast: [], pathLast: "a", generics: [], - typeFilter: -1, + typeFilter: null, }], ], ], - typeFilter: -1, + typeFilter: null, }], foundElems: 1, userQuery: "(aaaaa->a)", @@ -233,7 +233,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "aaaaa", generics: [], - typeFilter: -1, + typeFilter: null, }, { name: "b", @@ -241,7 +241,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "b", generics: [], - typeFilter: -1, + typeFilter: null, }, ], bindings: [ @@ -253,11 +253,11 @@ const PARSED = [ pathWithoutLast: [], pathLast: "a", generics: [], - typeFilter: -1, + typeFilter: null, }], ], ], - typeFilter: -1, + typeFilter: null, }], foundElems: 1, userQuery: "(aaaaa, b -> a)", @@ -278,7 +278,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "aaaaa", generics: [], - typeFilter: -1, + typeFilter: null, }, { name: "b", @@ -286,7 +286,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "b", generics: [], - typeFilter: -1, + typeFilter: null, }, ], bindings: [ @@ -298,11 +298,11 @@ const PARSED = [ pathWithoutLast: [], pathLast: "a", generics: [], - typeFilter: -1, + typeFilter: null, }], ], ], - typeFilter: 1, + typeFilter: "primitive", }], foundElems: 1, userQuery: "primitive:(aaaaa, b -> a)", @@ -318,7 +318,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "x", generics: [], - typeFilter: -1, + typeFilter: null, }, { name: "->", @@ -332,7 +332,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "aaaaa", generics: [], - typeFilter: -1, + typeFilter: null, }, { name: "b", @@ -340,7 +340,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "b", generics: [], - typeFilter: -1, + typeFilter: null, }, ], bindings: [ @@ -352,11 +352,11 @@ const PARSED = [ pathWithoutLast: [], pathLast: "a", generics: [], - typeFilter: -1, + typeFilter: null, }], ], ], - typeFilter: 10, + typeFilter: "trait", } ], foundElems: 2, @@ -390,11 +390,11 @@ const PARSED = [ generics: [], }, ], - typeFilter: -1, + typeFilter: null, }], ], ], - typeFilter: -1, + typeFilter: null, }], foundElems: 1, userQuery: "Fn () -> F<P>", @@ -418,11 +418,11 @@ const PARSED = [ pathWithoutLast: [], pathLast: "p", generics: [], - typeFilter: -1, + typeFilter: null, }], ], ], - typeFilter: -1, + typeFilter: null, }], foundElems: 1, userQuery: "FnMut() -> P", @@ -446,11 +446,11 @@ const PARSED = [ pathWithoutLast: [], pathLast: "p", generics: [], - typeFilter: -1, + typeFilter: null, }], ], ], - typeFilter: -1, + typeFilter: null, }], foundElems: 1, userQuery: "(FnMut() -> P)", @@ -478,7 +478,7 @@ const PARSED = [ generics: [], }, ], - typeFilter: -1, + typeFilter: null, }], bindings: [ [ @@ -486,7 +486,7 @@ const PARSED = [ [], ], ], - typeFilter: -1, + typeFilter: null, }], foundElems: 1, userQuery: "Fn(F<P>)", @@ -507,7 +507,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "aaaaa", generics: [], - typeFilter: -1, + typeFilter: null, }, { name: "b", @@ -515,7 +515,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "b", generics: [], - typeFilter: -1, + typeFilter: null, }, ], bindings: [ @@ -527,11 +527,11 @@ const PARSED = [ pathWithoutLast: [], pathLast: "a", generics: [], - typeFilter: -1, + typeFilter: null, }], ], ], - typeFilter: 1, + typeFilter: "primitive", }], foundElems: 1, userQuery: "primitive:fnonce(aaaaa, b) -> a", @@ -552,7 +552,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "aaaaa", generics: [], - typeFilter: -1, + typeFilter: null, }, { name: "b", @@ -560,7 +560,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "b", generics: [], - typeFilter: 0, + typeFilter: "keyword", }, ], bindings: [ @@ -572,11 +572,11 @@ const PARSED = [ pathWithoutLast: [], pathLast: "a", generics: [], - typeFilter: 10, + typeFilter: "trait", }], ], ], - typeFilter: 1, + typeFilter: "primitive", }], foundElems: 1, userQuery: "primitive:fnonce(aaaaa, keyword:b) -> trait:a", @@ -592,7 +592,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "x", generics: [], - typeFilter: -1, + typeFilter: null, }, { name: "fn", @@ -612,7 +612,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "aaaaa", generics: [], - typeFilter: -1, + typeFilter: null, }, { name: "b", @@ -620,7 +620,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "b", generics: [], - typeFilter: -1, + typeFilter: null, }, ], bindings: [ @@ -632,11 +632,11 @@ const PARSED = [ pathWithoutLast: [], pathLast: "a", generics: [], - typeFilter: -1, + typeFilter: null, }], ], ], - typeFilter: -1, + typeFilter: null, }, ], bindings: [ @@ -645,7 +645,7 @@ const PARSED = [ [], ] ], - typeFilter: 10, + typeFilter: "trait", } ], foundElems: 2, @@ -662,7 +662,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "a", generics: [], - typeFilter: -1, + typeFilter: null, }, { name: "b", @@ -675,7 +675,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "c", generics: [], - typeFilter: -1, + typeFilter: null, }], bindings: [ [ @@ -683,7 +683,7 @@ const PARSED = [ [], ] ], - typeFilter: -1, + typeFilter: null, } ], foundElems: 2, diff --git a/tests/rustdoc-js-std/parser-ident.js b/tests/rustdoc-js-std/parser-ident.js index f65391b157188..5366ac847b011 100644 --- a/tests/rustdoc-js-std/parser-ident.js +++ b/tests/rustdoc-js-std/parser-ident.js @@ -13,10 +13,10 @@ const PARSED = [ pathWithoutLast: [], pathLast: "never", generics: [], - typeFilter: 1, + typeFilter: "primitive", }, ], - typeFilter: -1, + typeFilter: null, }], foundElems: 1, userQuery: "R<!>", @@ -31,7 +31,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "never", generics: [], - typeFilter: 1, + typeFilter: "primitive", }], foundElems: 1, userQuery: "!", @@ -46,7 +46,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "a", generics: [], - typeFilter: 16, + typeFilter: "macro", }], foundElems: 1, userQuery: "a!", @@ -77,7 +77,7 @@ const PARSED = [ pathWithoutLast: ["never"], pathLast: "b", generics: [], - typeFilter: -1, + typeFilter: null, }], foundElems: 1, userQuery: "!::b", @@ -122,10 +122,10 @@ const PARSED = [ pathWithoutLast: [], pathLast: "t", generics: [], - typeFilter: -1, + typeFilter: null, } ], - typeFilter: -1, + typeFilter: null, }], foundElems: 1, userQuery: "!::b<T>", diff --git a/tests/rustdoc-js-std/parser-literal.js b/tests/rustdoc-js-std/parser-literal.js index 63e07a246a13d..803929b74fd44 100644 --- a/tests/rustdoc-js-std/parser-literal.js +++ b/tests/rustdoc-js-std/parser-literal.js @@ -15,7 +15,7 @@ const PARSED = [ generics: [], }, ], - typeFilter: -1, + typeFilter: null, }], foundElems: 1, userQuery: "R<P>", diff --git a/tests/rustdoc-js-std/parser-paths.js b/tests/rustdoc-js-std/parser-paths.js index bb34e22e518a4..3ddd6572277ed 100644 --- a/tests/rustdoc-js-std/parser-paths.js +++ b/tests/rustdoc-js-std/parser-paths.js @@ -7,7 +7,7 @@ const PARSED = [ pathWithoutLast: ["a"], pathLast: "b", generics: [], - typeFilter: -1, + typeFilter: null, }], foundElems: 1, userQuery: "A::B", @@ -22,7 +22,7 @@ const PARSED = [ pathWithoutLast: ["a"], pathLast: "a", generics: [], - typeFilter: -1, + typeFilter: null, }], foundElems: 1, userQuery: 'a:: a', @@ -37,7 +37,7 @@ const PARSED = [ pathWithoutLast: ["a"], pathLast: "a", generics: [], - typeFilter: -1, + typeFilter: null, }], foundElems: 1, userQuery: 'a ::a', @@ -52,7 +52,7 @@ const PARSED = [ pathWithoutLast: ["a"], pathLast: "a", generics: [], - typeFilter: -1, + typeFilter: null, }], foundElems: 1, userQuery: 'a :: a', @@ -68,7 +68,7 @@ const PARSED = [ pathWithoutLast: ["a"], pathLast: "b", generics: [], - typeFilter: -1, + typeFilter: null, }, { name: "C", @@ -76,7 +76,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "c", generics: [], - typeFilter: -1, + typeFilter: null, }, ], foundElems: 2, @@ -101,7 +101,7 @@ const PARSED = [ generics: [], }, ], - typeFilter: -1, + typeFilter: null, }, { name: "C", @@ -109,7 +109,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "c", generics: [], - typeFilter: -1, + typeFilter: null, }, ], foundElems: 2, @@ -125,7 +125,7 @@ const PARSED = [ pathWithoutLast: ["mod"], pathLast: "a", generics: [], - typeFilter: -1, + typeFilter: null, }], foundElems: 1, userQuery: "mod::a", diff --git a/tests/rustdoc-js-std/parser-quote.js b/tests/rustdoc-js-std/parser-quote.js index b485047e385eb..d5a9863367fe8 100644 --- a/tests/rustdoc-js-std/parser-quote.js +++ b/tests/rustdoc-js-std/parser-quote.js @@ -10,7 +10,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "p", generics: [], - typeFilter: -1, + typeFilter: null, }], error: null, }, @@ -22,7 +22,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "p", generics: [], - typeFilter: -1, + typeFilter: null, }], foundElems: 1, userQuery: '"p",', diff --git a/tests/rustdoc-js-std/parser-reference.js b/tests/rustdoc-js-std/parser-reference.js index 0fa07ae989528..b17dad5fb1f7d 100644 --- a/tests/rustdoc-js-std/parser-reference.js +++ b/tests/rustdoc-js-std/parser-reference.js @@ -42,16 +42,16 @@ const PARSED = [ pathWithoutLast: [], pathLast: "d", generics: [], - typeFilter: -1, + typeFilter: null, }, ], - typeFilter: 1, + typeFilter: "primitive", }, ], - typeFilter: 1, + typeFilter: "primitive", }, ], - typeFilter: 1, + typeFilter: "primitive", }, { name: "[]", @@ -59,7 +59,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "[]", generics: [], - typeFilter: 1, + typeFilter: "primitive", }, ], foundElems: 2, @@ -100,19 +100,19 @@ const PARSED = [ pathWithoutLast: [], pathLast: "d", generics: [], - typeFilter: -1, + typeFilter: null, }, ], - typeFilter: 1, + typeFilter: "primitive", }, ], - typeFilter: 1, + typeFilter: "primitive", }, ], - typeFilter: 1, + typeFilter: "primitive", }, ], - typeFilter: 1, + typeFilter: "primitive", }, ], foundElems: 1, @@ -129,7 +129,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "reference", generics: [], - typeFilter: 1, + typeFilter: "primitive", }, ], foundElems: 1, @@ -152,10 +152,10 @@ const PARSED = [ pathWithoutLast: [], pathLast: "mut", generics: [], - typeFilter: 0, + typeFilter: "keyword", }, ], - typeFilter: 1, + typeFilter: "primitive", }, ], foundElems: 1, @@ -172,7 +172,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "reference", generics: [], - typeFilter: 1, + typeFilter: "primitive", }, { name: "u8", @@ -180,7 +180,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "u8", generics: [], - typeFilter: -1, + typeFilter: null, }, ], foundElems: 2, @@ -203,10 +203,10 @@ const PARSED = [ pathWithoutLast: [], pathLast: "mut", generics: [], - typeFilter: 0, + typeFilter: "keyword", }, ], - typeFilter: 1, + typeFilter: "primitive", }, { name: "u8", @@ -214,7 +214,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "u8", generics: [], - typeFilter: -1, + typeFilter: null, }, ], foundElems: 2, @@ -237,10 +237,10 @@ const PARSED = [ pathWithoutLast: [], pathLast: "u8", generics: [], - typeFilter: -1, + typeFilter: null, }, ], - typeFilter: 1, + typeFilter: "primitive", }, ], foundElems: 1, @@ -269,13 +269,13 @@ const PARSED = [ pathWithoutLast: [], pathLast: "u8", generics: [], - typeFilter: -1, + typeFilter: null, }, ], - typeFilter: -1, + typeFilter: null, }, ], - typeFilter: 1, + typeFilter: "primitive", }, ], foundElems: 1, @@ -304,13 +304,13 @@ const PARSED = [ pathWithoutLast: [], pathLast: "u8", generics: [], - typeFilter: -1, + typeFilter: null, }, ], - typeFilter: 1, + typeFilter: "primitive", }, ], - typeFilter: -1, + typeFilter: null, }, ], foundElems: 1, @@ -339,10 +339,10 @@ const PARSED = [ pathWithoutLast: [], pathLast: "u8", generics: [], - typeFilter: -1, + typeFilter: null, }, ], - typeFilter: 1, + typeFilter: "primitive", }, { name: "u8", @@ -350,10 +350,10 @@ const PARSED = [ pathWithoutLast: [], pathLast: "u8", generics: [], - typeFilter: -1, + typeFilter: null, }, ], - typeFilter: -1, + typeFilter: null, }, ], foundElems: 1, @@ -382,13 +382,13 @@ const PARSED = [ pathWithoutLast: [], pathLast: "u8", generics: [], - typeFilter: -1, + typeFilter: null, }, ], - typeFilter: 1, + typeFilter: "primitive", }, ], - typeFilter: -1, + typeFilter: null, }, ], foundElems: 1, @@ -417,7 +417,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "mut", generics: [], - typeFilter: 0, + typeFilter: "keyword", }, { name: "u8", @@ -425,10 +425,10 @@ const PARSED = [ pathWithoutLast: [], pathLast: "u8", generics: [], - typeFilter: -1, + typeFilter: null, }, ], - typeFilter: 1, + typeFilter: "primitive", }, { name: "u8", @@ -436,10 +436,10 @@ const PARSED = [ pathWithoutLast: [], pathLast: "u8", generics: [], - typeFilter: -1, + typeFilter: null, }, ], - typeFilter: -1, + typeFilter: null, }, ], foundElems: 1, @@ -462,10 +462,10 @@ const PARSED = [ pathWithoutLast: [], pathLast: "u8", generics: [], - typeFilter: -1, + typeFilter: null, }, ], - typeFilter: 1, + typeFilter: "primitive", }, ], foundElems: 1, @@ -496,10 +496,10 @@ const PARSED = [ pathWithoutLast: [], pathLast: "u8", generics: [], - typeFilter: 16, + typeFilter: "macro", }, ], - typeFilter: 1, + typeFilter: "primitive", }, ], foundElems: 1, diff --git a/tests/rustdoc-js-std/parser-returned.js b/tests/rustdoc-js-std/parser-returned.js index 30ce26a892059..67aabdacb0413 100644 --- a/tests/rustdoc-js-std/parser-returned.js +++ b/tests/rustdoc-js-std/parser-returned.js @@ -18,7 +18,7 @@ const PARSED = [ generics: [], }, ], - typeFilter: -1, + typeFilter: null, }], error: null, }, @@ -33,7 +33,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "p", generics: [], - typeFilter: -1, + typeFilter: null, }], error: null, }, @@ -48,7 +48,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "a", generics: [], - typeFilter: -1, + typeFilter: null, }], error: null, }, @@ -60,7 +60,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "aaaaa", generics: [], - typeFilter: -1, + typeFilter: null, }], foundElems: 2, userQuery: "aaaaa->a", @@ -70,7 +70,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "a", generics: [], - typeFilter: -1, + typeFilter: null, }], error: null, }, @@ -85,7 +85,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "never", generics: [], - typeFilter: 1, + typeFilter: "primitive", }], error: null, }, @@ -97,7 +97,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "a", generics: [], - typeFilter: -1, + typeFilter: null, }], foundElems: 1, userQuery: "a->", @@ -113,7 +113,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "never", generics: [], - typeFilter: 1, + typeFilter: "primitive", }], foundElems: 1, userQuery: "!->", @@ -129,7 +129,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "never", generics: [], - typeFilter: 1, + typeFilter: "primitive", }], foundElems: 1, userQuery: "! ->", @@ -145,7 +145,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "never", generics: [], - typeFilter: 1, + typeFilter: "primitive", }], foundElems: 1, userQuery: "primitive:!->", diff --git a/tests/rustdoc-js-std/parser-separators.js b/tests/rustdoc-js-std/parser-separators.js index cf271c80cdce2..2f41211d78310 100644 --- a/tests/rustdoc-js-std/parser-separators.js +++ b/tests/rustdoc-js-std/parser-separators.js @@ -10,7 +10,7 @@ const PARSED = [ pathWithoutLast: ['aaaaaa'], pathLast: 'b', generics: [], - typeFilter: -1, + typeFilter: null, }, ], foundElems: 1, @@ -27,7 +27,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: 'aaaaaa', generics: [], - typeFilter: -1, + typeFilter: null, }, { name: 'b', @@ -35,7 +35,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: 'b', generics: [], - typeFilter: -1, + typeFilter: null, }, ], foundElems: 2, @@ -52,7 +52,7 @@ const PARSED = [ pathWithoutLast: ['a'], pathLast: 'b', generics: [], - typeFilter: -1, + typeFilter: null, }, ], foundElems: 1, @@ -69,7 +69,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: 'a', generics: [], - typeFilter: -1, + typeFilter: null, }, { name: 'b', @@ -77,7 +77,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: 'b', generics: [], - typeFilter: -1, + typeFilter: null, }, ], foundElems: 2, @@ -94,7 +94,7 @@ const PARSED = [ pathWithoutLast: ['a'], pathLast: 'b', generics: [], - typeFilter: -1, + typeFilter: null, }, ], foundElems: 1, @@ -119,7 +119,7 @@ const PARSED = [ generics: [], }, ], - typeFilter: -1, + typeFilter: null, }, ], foundElems: 1, @@ -151,7 +151,7 @@ const PARSED = [ generics: [], }, ], - typeFilter: -1, + typeFilter: null, }, ], foundElems: 1, @@ -176,7 +176,7 @@ const PARSED = [ generics: [], }, ], - typeFilter: -1, + typeFilter: null, }, ], foundElems: 1, diff --git a/tests/rustdoc-js-std/parser-slice-array.js b/tests/rustdoc-js-std/parser-slice-array.js index 6579794553599..c587eb9001f37 100644 --- a/tests/rustdoc-js-std/parser-slice-array.js +++ b/tests/rustdoc-js-std/parser-slice-array.js @@ -34,7 +34,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "d", generics: [], - typeFilter: -1, + typeFilter: null, }, { name: "[]", @@ -42,16 +42,16 @@ const PARSED = [ pathWithoutLast: [], pathLast: "[]", generics: [], - typeFilter: 1, + typeFilter: "primitive", }, ], - typeFilter: 1, + typeFilter: "primitive", }, ], - typeFilter: 1, + typeFilter: "primitive", }, ], - typeFilter: 1, + typeFilter: "primitive", }, ], foundElems: 1, @@ -68,7 +68,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "[]", generics: [], - typeFilter: 1, + typeFilter: "primitive", }, { name: "u8", @@ -76,7 +76,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "u8", generics: [], - typeFilter: -1, + typeFilter: null, }, ], foundElems: 2, @@ -99,10 +99,10 @@ const PARSED = [ pathWithoutLast: [], pathLast: "u8", generics: [], - typeFilter: -1, + typeFilter: null, }, ], - typeFilter: 1, + typeFilter: "primitive", }, ], foundElems: 1, @@ -125,7 +125,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "u8", generics: [], - typeFilter: -1, + typeFilter: null, }, { name: "u8", @@ -133,10 +133,10 @@ const PARSED = [ pathWithoutLast: [], pathLast: "u8", generics: [], - typeFilter: -1, + typeFilter: null, }, ], - typeFilter: 1, + typeFilter: "primitive", }, ], foundElems: 1, @@ -165,13 +165,13 @@ const PARSED = [ pathWithoutLast: [], pathLast: "u8", generics: [], - typeFilter: -1, + typeFilter: null, }, ], - typeFilter: -1, + typeFilter: null, }, ], - typeFilter: 1, + typeFilter: "primitive", }, ], foundElems: 1, @@ -188,7 +188,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "[]", generics: [], - typeFilter: 1, + typeFilter: "primitive", }, ], foundElems: 1, @@ -283,10 +283,10 @@ const PARSED = [ pathWithoutLast: [], pathLast: "u8", generics: [], - typeFilter: -1, + typeFilter: null, }, ], - typeFilter: 1, + typeFilter: "primitive", }, ], foundElems: 1, diff --git a/tests/rustdoc-js-std/parser-tuple.js b/tests/rustdoc-js-std/parser-tuple.js index 6192506838708..dfe9fdc98e3a6 100644 --- a/tests/rustdoc-js-std/parser-tuple.js +++ b/tests/rustdoc-js-std/parser-tuple.js @@ -22,7 +22,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "d", generics: [], - typeFilter: -1, + typeFilter: null, }, { name: "()", @@ -30,10 +30,10 @@ const PARSED = [ pathWithoutLast: [], pathLast: "()", generics: [], - typeFilter: 1, + typeFilter: "primitive", }, ], - typeFilter: 1, + typeFilter: "primitive", } ], foundElems: 1, @@ -50,7 +50,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "()", generics: [], - typeFilter: 1, + typeFilter: "primitive", }, { name: "u8", @@ -58,7 +58,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "u8", generics: [], - typeFilter: -1, + typeFilter: null, }, ], foundElems: 2, @@ -81,7 +81,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "u8", generics: [], - typeFilter: -1, + typeFilter: null, }, ], foundElems: 1, @@ -104,10 +104,10 @@ const PARSED = [ pathWithoutLast: [], pathLast: "u8", generics: [], - typeFilter: -1, + typeFilter: null, }, ], - typeFilter: 1, + typeFilter: "primitive", }, ], foundElems: 1, @@ -130,10 +130,10 @@ const PARSED = [ pathWithoutLast: [], pathLast: "u8", generics: [], - typeFilter: -1, + typeFilter: null, }, ], - typeFilter: 1, + typeFilter: "primitive", }, ], foundElems: 1, @@ -156,10 +156,10 @@ const PARSED = [ pathWithoutLast: [], pathLast: "u8", generics: [], - typeFilter: -1, + typeFilter: null, }, ], - typeFilter: 1, + typeFilter: "primitive", }, ], foundElems: 1, @@ -176,7 +176,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "u8", generics: [], - typeFilter: 1, + typeFilter: "primitive", }, ], foundElems: 1, @@ -199,7 +199,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "u8", generics: [], - typeFilter: -1, + typeFilter: null, }, { name: "u8", @@ -207,10 +207,10 @@ const PARSED = [ pathWithoutLast: [], pathLast: "u8", generics: [], - typeFilter: -1, + typeFilter: null, }, ], - typeFilter: 1, + typeFilter: "primitive", }, ], foundElems: 1, @@ -233,10 +233,10 @@ const PARSED = [ pathWithoutLast: [], pathLast: "u8", generics: [], - typeFilter: -1, + typeFilter: null, }, ], - typeFilter: -1, + typeFilter: null, }, ], foundElems: 1, @@ -253,7 +253,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "()", generics: [], - typeFilter: 1, + typeFilter: "primitive", }, ], foundElems: 1, diff --git a/tests/rustdoc-js-std/path-end-empty.js b/tests/rustdoc-js-std/path-end-empty.js index 6e853c61b4d95..17b8cac8a6681 100644 --- a/tests/rustdoc-js-std/path-end-empty.js +++ b/tests/rustdoc-js-std/path-end-empty.js @@ -1,6 +1,7 @@ +const FILTER_CRATE = "std"; const EXPECTED = { 'query': 'Option::', 'others': [ - { 'path': 'std::option::Option', 'name': 'get_or_insert_default' }, + { 'path': 'std::option::Option', 'name': 'eq' }, ], } diff --git a/tests/rustdoc-js-std/path-maxeditdistance.js b/tests/rustdoc-js-std/path-maxeditdistance.js index 6989e7d6488b0..b22a506eee5fa 100644 --- a/tests/rustdoc-js-std/path-maxeditdistance.js +++ b/tests/rustdoc-js-std/path-maxeditdistance.js @@ -10,15 +10,15 @@ const EXPECTED = [ query: 'vec::iter', others: [ // std::net::ToSocketAttrs::iter should not show up here - { 'path': 'std::vec', 'name': 'IntoIter' }, + { 'path': 'std::collections::VecDeque', 'name': 'iter' }, + { 'path': 'std::collections::VecDeque', 'name': 'iter_mut' }, { 'path': 'std::vec::Vec', 'name': 'from_iter' }, + { 'path': 'std::vec', 'name': 'IntoIter' }, { 'path': 'std::vec::Vec', 'name': 'into_iter' }, { 'path': 'std::vec::ExtractIf', 'name': 'into_iter' }, { 'path': 'std::vec::Drain', 'name': 'into_iter' }, { 'path': 'std::vec::IntoIter', 'name': 'into_iter' }, { 'path': 'std::vec::Splice', 'name': 'into_iter' }, - { 'path': 'std::collections::VecDeque', 'name': 'iter' }, - { 'path': 'std::collections::VecDeque', 'name': 'iter_mut' }, { 'path': 'std::collections::VecDeque', 'name': 'from_iter' }, { 'path': 'std::collections::VecDeque', 'name': 'into_iter' }, ], diff --git a/tests/rustdoc-js-std/return-specific-literal.js b/tests/rustdoc-js-std/return-specific-literal.js index 86ed3aceb4e84..1efdb776ad73f 100644 --- a/tests/rustdoc-js-std/return-specific-literal.js +++ b/tests/rustdoc-js-std/return-specific-literal.js @@ -4,6 +4,6 @@ const EXPECTED = { { 'path': 'std::string::String', 'name': 'ne' }, ], 'returned': [ - { 'path': 'std::string::String', 'name': 'add' }, + { 'path': 'std::string::String', 'name': 'new' }, ], }; diff --git a/tests/rustdoc-js-std/return-specific.js b/tests/rustdoc-js-std/return-specific.js index be54a1c977254..abf243bf6ab73 100644 --- a/tests/rustdoc-js-std/return-specific.js +++ b/tests/rustdoc-js-std/return-specific.js @@ -4,6 +4,6 @@ const EXPECTED = { { 'path': 'std::string::String', 'name': 'ne' }, ], 'returned': [ - { 'path': 'std::string::String', 'name': 'add' }, + { 'path': 'std::string::String', 'name': 'new' }, ], }; diff --git a/tests/rustdoc-js/generics-trait.js b/tests/rustdoc-js/generics-trait.js index 8da9c67050e62..cd100463e9a85 100644 --- a/tests/rustdoc-js/generics-trait.js +++ b/tests/rustdoc-js/generics-trait.js @@ -25,8 +25,25 @@ const EXPECTED = [ }, { 'query': 'Resulx<SomeTrait>', - 'in_args': [], - 'returned': [], + 'correction': 'Result', + 'in_args': [ + { + 'path': 'generics_trait', + 'name': 'beta', + 'displayType': '`Result`<`T`, ()> -> ()', + 'displayMappedNames': '', + 'displayWhereClause': 'T: `SomeTrait`', + }, + ], + 'returned': [ + { + 'path': 'generics_trait', + 'name': 'bet', + 'displayType': ' -> `Result`<`T`, ()>', + 'displayMappedNames': '', + 'displayWhereClause': 'T: `SomeTrait`', + }, + ], }, { 'query': 'Result<SomeTraiz>', diff --git a/tests/rustdoc-js/non-english-identifier.js b/tests/rustdoc-js/non-english-identifier.js index 3d50bd3ee9057..48542e04a9c8e 100644 --- a/tests/rustdoc-js/non-english-identifier.js +++ b/tests/rustdoc-js/non-english-identifier.js @@ -7,7 +7,7 @@ const PARSED = [ pathWithoutLast: [], pathLast: "中文", generics: [], - typeFilter: -1, + typeFilter: null, }], returned: [], foundElems: 1, @@ -23,7 +23,7 @@ const PARSED = [ pathLast: "_0mixed中英文", normalizedPathLast: "0mixed中英文", generics: [], - typeFilter: -1, + typeFilter: null, }], foundElems: 1, userQuery: "_0Mixed中英文", @@ -38,7 +38,7 @@ const PARSED = [ pathWithoutLast: ["my_crate"], pathLast: "中文api", generics: [], - typeFilter: -1, + typeFilter: null, }], foundElems: 1, userQuery: "my_crate::中文API", @@ -94,7 +94,7 @@ const PARSED = [ pathWithoutLast: ["my_crate"], pathLast: "中文宏", generics: [], - typeFilter: 16, + typeFilter: "macro", }], foundElems: 1, userQuery: "my_crate 中文宏!", @@ -114,12 +114,6 @@ const EXPECTED = [ { query: '加法', others: [ - { - name: "加法", - path: "non_english_identifier", - href: "../non_english_identifier/trait.加法.html", - desc: "Add" - }, { name: "add", path: "non_english_identifier", @@ -134,6 +128,12 @@ const EXPECTED = [ alias: "加法", href: "../non_english_identifier/macro.add.html" }, + { + name: "加法", + path: "non_english_identifier", + href: "../non_english_identifier/trait.加法.html", + desc: "Add" + }, ], in_args: [{ name: "加上", diff --git a/tests/rustdoc-js/type-parameters.js b/tests/rustdoc-js/type-parameters.js index fa2b8d2ebfdb6..b1f1ee951c6e5 100644 --- a/tests/rustdoc-js/type-parameters.js +++ b/tests/rustdoc-js/type-parameters.js @@ -4,8 +4,8 @@ const EXPECTED = [ { query: '-> trait:Some', others: [ - { path: 'foo', name: 'alpha' }, { path: 'foo', name: 'alef' }, + { path: 'foo', name: 'alpha' }, ], }, { @@ -75,10 +75,8 @@ const EXPECTED = [ { query: 'Other', in_args: [ - // because function is called "other", it's sorted first - // even though it has higher type distance - { path: 'foo', name: 'other' }, { path: 'foo', name: 'alternate' }, + { path: 'foo', name: 'other' }, ], }, { diff --git a/tests/rustdoc/cross-crate-info/cargo-transitive-no-index/s.rs b/tests/rustdoc/cross-crate-info/cargo-transitive-no-index/s.rs index 85c460ace642f..088ab242d2769 100644 --- a/tests/rustdoc/cross-crate-info/cargo-transitive-no-index/s.rs +++ b/tests/rustdoc/cross-crate-info/cargo-transitive-no-index/s.rs @@ -5,9 +5,9 @@ //@ has t/trait.Tango.html //@ hasraw s/struct.Sierra.html 'Tango' //@ hasraw trait.impl/t/trait.Tango.js 'struct.Sierra.html' -//@ hasraw search-index.js 'Tango' -//@ hasraw search-index.js 'Sierra' -//@ hasraw search-index.js 'Quebec' +//@ hasraw search.index/name/*.js 'Tango' +//@ hasraw search.index/name/*.js 'Sierra' +//@ hasraw search.index/name/*.js 'Quebec' // We document multiple crates into the same output directory, which // merges the cross-crate information. Everything is available. diff --git a/tests/rustdoc/cross-crate-info/cargo-transitive/s.rs b/tests/rustdoc/cross-crate-info/cargo-transitive/s.rs index 68bfc34883bd8..fb6eef0bf691b 100644 --- a/tests/rustdoc/cross-crate-info/cargo-transitive/s.rs +++ b/tests/rustdoc/cross-crate-info/cargo-transitive/s.rs @@ -13,9 +13,9 @@ //@ has t/trait.Tango.html //@ hasraw s/struct.Sierra.html 'Tango' //@ hasraw trait.impl/t/trait.Tango.js 'struct.Sierra.html' -//@ hasraw search-index.js 'Tango' -//@ hasraw search-index.js 'Sierra' -//@ hasraw search-index.js 'Quebec' +//@ hasraw search.index/name/*.js 'Tango' +//@ hasraw search.index/name/*.js 'Sierra' +//@ hasraw search.index/name/*.js 'Quebec' // We document multiple crates into the same output directory, which // merges the cross-crate information. Everything is available. diff --git a/tests/rustdoc/cross-crate-info/cargo-two-no-index/e.rs b/tests/rustdoc/cross-crate-info/cargo-two-no-index/e.rs index c93298f969eab..533756705526f 100644 --- a/tests/rustdoc/cross-crate-info/cargo-two-no-index/e.rs +++ b/tests/rustdoc/cross-crate-info/cargo-two-no-index/e.rs @@ -4,8 +4,8 @@ //@ has f/trait.Foxtrot.html //@ hasraw e/enum.Echo.html 'Foxtrot' //@ hasraw trait.impl/f/trait.Foxtrot.js 'enum.Echo.html' -//@ hasraw search-index.js 'Foxtrot' -//@ hasraw search-index.js 'Echo' +//@ hasraw search.index/name/*.js 'Foxtrot' +//@ hasraw search.index/name/*.js 'Echo' // document two crates in the same way that cargo does. do not provide // --enable-index-page diff --git a/tests/rustdoc/cross-crate-info/cargo-two/e.rs b/tests/rustdoc/cross-crate-info/cargo-two/e.rs index 00f86cbc34889..936e75c97af72 100644 --- a/tests/rustdoc/cross-crate-info/cargo-two/e.rs +++ b/tests/rustdoc/cross-crate-info/cargo-two/e.rs @@ -11,8 +11,8 @@ //@ has f/trait.Foxtrot.html //@ hasraw e/enum.Echo.html 'Foxtrot' //@ hasraw trait.impl/f/trait.Foxtrot.js 'enum.Echo.html' -//@ hasraw search-index.js 'Foxtrot' -//@ hasraw search-index.js 'Echo' +//@ hasraw search.index/name/*.js 'Foxtrot' +//@ hasraw search.index/name/*.js 'Echo' // document two crates in the same way that cargo does, writing them both // into the same output directory diff --git a/tests/rustdoc/cross-crate-info/index-on-last/e.rs b/tests/rustdoc/cross-crate-info/index-on-last/e.rs index ffee898cd966d..dbaeaf5b72536 100644 --- a/tests/rustdoc/cross-crate-info/index-on-last/e.rs +++ b/tests/rustdoc/cross-crate-info/index-on-last/e.rs @@ -11,8 +11,8 @@ //@ has f/trait.Foxtrot.html //@ hasraw e/enum.Echo.html 'Foxtrot' //@ hasraw trait.impl/f/trait.Foxtrot.js 'enum.Echo.html' -//@ hasraw search-index.js 'Foxtrot' -//@ hasraw search-index.js 'Echo' +//@ hasraw search.index/name/*.js 'Foxtrot' +//@ hasraw search.index/name/*.js 'Echo' // only declare --enable-index-page to the last rustdoc invocation extern crate f; diff --git a/tests/rustdoc/cross-crate-info/kitchen-sink/i.rs b/tests/rustdoc/cross-crate-info/kitchen-sink/i.rs index bcb9464795af2..979d77d8c42ea 100644 --- a/tests/rustdoc/cross-crate-info/kitchen-sink/i.rs +++ b/tests/rustdoc/cross-crate-info/kitchen-sink/i.rs @@ -19,10 +19,10 @@ //@ has t/trait.Tango.html //@ hasraw s/struct.Sierra.html 'Tango' //@ hasraw trait.impl/t/trait.Tango.js 'struct.Sierra.html' -//@ hasraw search-index.js 'Quebec' -//@ hasraw search-index.js 'Romeo' -//@ hasraw search-index.js 'Sierra' -//@ hasraw search-index.js 'Tango' +//@ hasraw search.index/name/*.js 'Quebec' +//@ hasraw search.index/name/*.js 'Romeo' +//@ hasraw search.index/name/*.js 'Sierra' +//@ hasraw search.index/name/*.js 'Tango' //@ has type.impl/s/struct.Sierra.js //@ hasraw type.impl/s/struct.Sierra.js 'Tango' //@ hasraw type.impl/s/struct.Sierra.js 'Romeo' diff --git a/tests/rustdoc/cross-crate-info/single-crate-baseline/q.rs b/tests/rustdoc/cross-crate-info/single-crate-baseline/q.rs index c5e3dc0a0f4e5..439ab23de185d 100644 --- a/tests/rustdoc/cross-crate-info/single-crate-baseline/q.rs +++ b/tests/rustdoc/cross-crate-info/single-crate-baseline/q.rs @@ -6,7 +6,7 @@ //@ has index.html '//h1' 'List of all crates' //@ has index.html '//ul[@class="all-items"]//a[@href="q/index.html"]' 'q' //@ has q/struct.Quebec.html -//@ hasraw search-index.js 'Quebec' +//@ hasraw search.index/name/*.js 'Quebec' // there's nothing cross-crate going on here pub struct Quebec; diff --git a/tests/rustdoc/cross-crate-info/single-crate-no-index/q.rs b/tests/rustdoc/cross-crate-info/single-crate-no-index/q.rs index d3e71fa0ce35f..b3703658465a4 100644 --- a/tests/rustdoc/cross-crate-info/single-crate-no-index/q.rs +++ b/tests/rustdoc/cross-crate-info/single-crate-no-index/q.rs @@ -1,6 +1,6 @@ //@ build-aux-docs //@ has q/struct.Quebec.html -//@ hasraw search-index.js 'Quebec' +//@ hasraw search.index/name/*.js 'Quebec' // there's nothing cross-crate going on here pub struct Quebec; diff --git a/tests/rustdoc/cross-crate-info/write-docs-somewhere-else/e.rs b/tests/rustdoc/cross-crate-info/write-docs-somewhere-else/e.rs index 9dcec211e1787..6ded19546b886 100644 --- a/tests/rustdoc/cross-crate-info/write-docs-somewhere-else/e.rs +++ b/tests/rustdoc/cross-crate-info/write-docs-somewhere-else/e.rs @@ -4,8 +4,8 @@ //@ !has f/trait.Foxtrot.html //@ hasraw e/enum.Echo.html 'Foxtrot' //@ hasraw trait.impl/f/trait.Foxtrot.js 'enum.Echo.html' -//@ !hasraw search-index.js 'Foxtrot' -//@ hasraw search-index.js 'Echo' +//@ !hasraw search.index/name/*.js 'Foxtrot' +//@ hasraw search.index/name/*.js 'Echo' // test the fact that our test runner will document this crate somewhere // else diff --git a/tests/rustdoc/masked.rs b/tests/rustdoc/masked.rs index 4f361ca881e3f..bc0a5f57cef0d 100644 --- a/tests/rustdoc/masked.rs +++ b/tests/rustdoc/masked.rs @@ -7,7 +7,7 @@ #[doc(masked)] extern crate masked; -//@ !hasraw 'search-index.js' 'masked_method' +//@ !hasraw 'search.index/name/*.js' 'masked_method' //@ !hasraw 'foo/struct.String.html' 'MaskedTrait' //@ !hasraw 'foo/struct.String.html' 'MaskedBlanketTrait' diff --git a/tests/rustdoc/merge-cross-crate-info/cargo-transitive-read-write/sierra.rs b/tests/rustdoc/merge-cross-crate-info/cargo-transitive-read-write/sierra.rs index 665f9567ba2d2..26292c50d35e6 100644 --- a/tests/rustdoc/merge-cross-crate-info/cargo-transitive-read-write/sierra.rs +++ b/tests/rustdoc/merge-cross-crate-info/cargo-transitive-read-write/sierra.rs @@ -14,9 +14,9 @@ //@ has tango/trait.Tango.html //@ hasraw sierra/struct.Sierra.html 'Tango' //@ hasraw trait.impl/tango/trait.Tango.js 'struct.Sierra.html' -//@ hasraw search-index.js 'Tango' -//@ hasraw search-index.js 'Sierra' -//@ hasraw search-index.js 'Quebec' +//@ hasraw search.index/name/*.js 'Tango' +//@ hasraw search.index/name/*.js 'Sierra' +//@ hasraw search.index/name/*.js 'Quebec' // similar to cargo-workflow-transitive, but we use --merge=read-write, // which is the default. diff --git a/tests/rustdoc/merge-cross-crate-info/kitchen-sink-separate-dirs/indigo.rs b/tests/rustdoc/merge-cross-crate-info/kitchen-sink-separate-dirs/indigo.rs index f03f6bd6026fa..fd6ee0cbf2407 100644 --- a/tests/rustdoc/merge-cross-crate-info/kitchen-sink-separate-dirs/indigo.rs +++ b/tests/rustdoc/merge-cross-crate-info/kitchen-sink-separate-dirs/indigo.rs @@ -23,10 +23,10 @@ //@ !has sierra/struct.Sierra.html //@ !has tango/trait.Tango.html //@ hasraw trait.impl/tango/trait.Tango.js 'struct.Sierra.html' -//@ hasraw search-index.js 'Quebec' -//@ hasraw search-index.js 'Romeo' -//@ hasraw search-index.js 'Sierra' -//@ hasraw search-index.js 'Tango' +//@ hasraw search.index/name/*.js 'Quebec' +//@ hasraw search.index/name/*.js 'Romeo' +//@ hasraw search.index/name/*.js 'Sierra' +//@ hasraw search.index/name/*.js 'Tango' //@ has type.impl/sierra/struct.Sierra.js //@ hasraw type.impl/sierra/struct.Sierra.js 'Tango' //@ hasraw type.impl/sierra/struct.Sierra.js 'Romeo' diff --git a/tests/rustdoc/merge-cross-crate-info/no-merge-separate/sierra.rs b/tests/rustdoc/merge-cross-crate-info/no-merge-separate/sierra.rs index 7eac207e51886..c3b8200f15107 100644 --- a/tests/rustdoc/merge-cross-crate-info/no-merge-separate/sierra.rs +++ b/tests/rustdoc/merge-cross-crate-info/no-merge-separate/sierra.rs @@ -8,7 +8,7 @@ //@ has sierra/struct.Sierra.html //@ hasraw sierra/struct.Sierra.html 'Tango' //@ !has trait.impl/tango/trait.Tango.js -//@ !has search-index.js +//@ !has search.index/name/*.js // we don't generate any cross-crate info if --merge=none, even if we // document crates separately diff --git a/tests/rustdoc/merge-cross-crate-info/no-merge-write-anyway/sierra.rs b/tests/rustdoc/merge-cross-crate-info/no-merge-write-anyway/sierra.rs index f3340a80c848b..2e47d42daff94 100644 --- a/tests/rustdoc/merge-cross-crate-info/no-merge-write-anyway/sierra.rs +++ b/tests/rustdoc/merge-cross-crate-info/no-merge-write-anyway/sierra.rs @@ -10,7 +10,7 @@ //@ has tango/trait.Tango.html //@ hasraw sierra/struct.Sierra.html 'Tango' //@ !has trait.impl/tango/trait.Tango.js -//@ !has search-index.js +//@ !has search.index/name/*.js // we --merge=none, so --parts-out-dir doesn't do anything extern crate tango; diff --git a/tests/rustdoc/merge-cross-crate-info/overwrite-but-include/sierra.rs b/tests/rustdoc/merge-cross-crate-info/overwrite-but-include/sierra.rs index 8eb0f1d049831..337dc558f35d8 100644 --- a/tests/rustdoc/merge-cross-crate-info/overwrite-but-include/sierra.rs +++ b/tests/rustdoc/merge-cross-crate-info/overwrite-but-include/sierra.rs @@ -10,9 +10,9 @@ //@ has tango/trait.Tango.html //@ hasraw sierra/struct.Sierra.html 'Tango' //@ hasraw trait.impl/tango/trait.Tango.js 'struct.Sierra.html' -//@ hasraw search-index.js 'Tango' -//@ hasraw search-index.js 'Sierra' -//@ !hasraw search-index.js 'Quebec' +//@ hasraw search.index/name/*.js 'Tango' +//@ hasraw search.index/name/*.js 'Sierra' +//@ !hasraw search.index/name/*.js 'Quebec' // we overwrite quebec and tango's cross-crate information, but we // include the info from tango meaning that it should appear in the out diff --git a/tests/rustdoc/merge-cross-crate-info/overwrite-but-separate/sierra.rs b/tests/rustdoc/merge-cross-crate-info/overwrite-but-separate/sierra.rs index 4ee036238b4b1..c07b30d2aa0cd 100644 --- a/tests/rustdoc/merge-cross-crate-info/overwrite-but-separate/sierra.rs +++ b/tests/rustdoc/merge-cross-crate-info/overwrite-but-separate/sierra.rs @@ -13,9 +13,9 @@ //@ has index.html '//ul[@class="all-items"]//a[@href="tango/index.html"]' 'tango' //@ has sierra/struct.Sierra.html //@ hasraw trait.impl/tango/trait.Tango.js 'struct.Sierra.html' -//@ hasraw search-index.js 'Tango' -//@ hasraw search-index.js 'Sierra' -//@ hasraw search-index.js 'Quebec' +//@ hasraw search.index/name/*.js 'Tango' +//@ hasraw search.index/name/*.js 'Sierra' +//@ hasraw search.index/name/*.js 'Quebec' // If these were documeted into the same directory, the info would be // overwritten. However, since they are merged, we can still recover all diff --git a/tests/rustdoc/merge-cross-crate-info/overwrite/sierra.rs b/tests/rustdoc/merge-cross-crate-info/overwrite/sierra.rs index 11e61dd2744b6..cac978f3bb272 100644 --- a/tests/rustdoc/merge-cross-crate-info/overwrite/sierra.rs +++ b/tests/rustdoc/merge-cross-crate-info/overwrite/sierra.rs @@ -9,9 +9,9 @@ //@ has tango/trait.Tango.html //@ hasraw sierra/struct.Sierra.html 'Tango' //@ hasraw trait.impl/tango/trait.Tango.js 'struct.Sierra.html' -//@ hasraw search-index.js 'Tango' -//@ hasraw search-index.js 'Sierra' -//@ !hasraw search-index.js 'Quebec' +//@ hasraw search.index/name/*.js 'Tango' +//@ hasraw search.index/name/*.js 'Sierra' +//@ !hasraw search.index/name/*.js 'Quebec' // since tango is documented with --merge=finalize, we overwrite q's // cross-crate information diff --git a/tests/rustdoc/merge-cross-crate-info/single-crate-finalize/quebec.rs b/tests/rustdoc/merge-cross-crate-info/single-crate-finalize/quebec.rs index 09bb78c06f1c2..2ab08c112a18c 100644 --- a/tests/rustdoc/merge-cross-crate-info/single-crate-finalize/quebec.rs +++ b/tests/rustdoc/merge-cross-crate-info/single-crate-finalize/quebec.rs @@ -6,7 +6,7 @@ //@ has index.html '//h1' 'List of all crates' //@ has index.html '//ul[@class="all-items"]//a[@href="quebec/index.html"]' 'quebec' //@ has quebec/struct.Quebec.html -//@ hasraw search-index.js 'Quebec' +//@ hasraw search.index/name/*.js 'Quebec' // there is nothing to read from the output directory if we use a single // crate diff --git a/tests/rustdoc/merge-cross-crate-info/single-crate-read-write/quebec.rs b/tests/rustdoc/merge-cross-crate-info/single-crate-read-write/quebec.rs index 72475426f6edd..1b9e8a3db08da 100644 --- a/tests/rustdoc/merge-cross-crate-info/single-crate-read-write/quebec.rs +++ b/tests/rustdoc/merge-cross-crate-info/single-crate-read-write/quebec.rs @@ -6,7 +6,7 @@ //@ has index.html '//h1' 'List of all crates' //@ has index.html '//ul[@class="all-items"]//a[@href="quebec/index.html"]' 'quebec' //@ has quebec/struct.Quebec.html -//@ hasraw search-index.js 'Quebec' +//@ hasraw search.index/name/*.js 'Quebec' // read-write is the default and this does the same as `single-crate` pub struct Quebec; diff --git a/tests/rustdoc/merge-cross-crate-info/single-crate-write-anyway/quebec.rs b/tests/rustdoc/merge-cross-crate-info/single-crate-write-anyway/quebec.rs index b20e173a8307e..6b72615eb9dec 100644 --- a/tests/rustdoc/merge-cross-crate-info/single-crate-write-anyway/quebec.rs +++ b/tests/rustdoc/merge-cross-crate-info/single-crate-write-anyway/quebec.rs @@ -6,7 +6,7 @@ //@ has index.html '//h1' 'List of all crates' //@ has index.html '//ul[@class="all-items"]//a[@href="quebec/index.html"]' 'quebec' //@ has quebec/struct.Quebec.html -//@ hasraw search-index.js 'Quebec' +//@ hasraw search.index/name/*.js 'Quebec' // we can --parts-out-dir, but that doesn't do anything other than create // the file diff --git a/tests/rustdoc/merge-cross-crate-info/single-merge-none-useless-write/quebec.rs b/tests/rustdoc/merge-cross-crate-info/single-merge-none-useless-write/quebec.rs index e888a43c46042..bfde21c9ed3c9 100644 --- a/tests/rustdoc/merge-cross-crate-info/single-merge-none-useless-write/quebec.rs +++ b/tests/rustdoc/merge-cross-crate-info/single-merge-none-useless-write/quebec.rs @@ -5,7 +5,7 @@ //@ !has index.html //@ has quebec/struct.Quebec.html -//@ !has search-index.js +//@ !has search.index/name/*.js // --merge=none doesn't write anything, despite --parts-out-dir pub struct Quebec; diff --git a/tests/rustdoc/merge-cross-crate-info/transitive-finalize/sierra.rs b/tests/rustdoc/merge-cross-crate-info/transitive-finalize/sierra.rs index 68fc4b13fa8f7..b45895a40a1b6 100644 --- a/tests/rustdoc/merge-cross-crate-info/transitive-finalize/sierra.rs +++ b/tests/rustdoc/merge-cross-crate-info/transitive-finalize/sierra.rs @@ -12,7 +12,7 @@ //@ has tango/trait.Tango.html //@ hasraw sierra/struct.Sierra.html 'Tango' //@ hasraw trait.impl/tango/trait.Tango.js 'struct.Sierra.html' -//@ hasraw search-index.js 'Sierra' +//@ hasraw search.index/name/*.js 'Sierra' // write only overwrites stuff in the output directory extern crate tango; diff --git a/tests/rustdoc/merge-cross-crate-info/transitive-merge-none/sierra.rs b/tests/rustdoc/merge-cross-crate-info/transitive-merge-none/sierra.rs index b407228085e88..be37137617936 100644 --- a/tests/rustdoc/merge-cross-crate-info/transitive-merge-none/sierra.rs +++ b/tests/rustdoc/merge-cross-crate-info/transitive-merge-none/sierra.rs @@ -16,9 +16,9 @@ //@ has tango/trait.Tango.html //@ hasraw sierra/struct.Sierra.html 'Tango' //@ hasraw trait.impl/tango/trait.Tango.js 'struct.Sierra.html' -//@ hasraw search-index.js 'Tango' -//@ hasraw search-index.js 'Sierra' -//@ hasraw search-index.js 'Quebec' +//@ hasraw search.index/name/*.js 'Tango' +//@ hasraw search.index/name/*.js 'Sierra' +//@ hasraw search.index/name/*.js 'Quebec' // We avoid writing any cross-crate information, preferring to include it // with --include-parts-dir. diff --git a/tests/rustdoc/merge-cross-crate-info/transitive-merge-read-write/sierra.rs b/tests/rustdoc/merge-cross-crate-info/transitive-merge-read-write/sierra.rs index 15e32d5941fb2..dc10ec3de35b0 100644 --- a/tests/rustdoc/merge-cross-crate-info/transitive-merge-read-write/sierra.rs +++ b/tests/rustdoc/merge-cross-crate-info/transitive-merge-read-write/sierra.rs @@ -14,9 +14,9 @@ //@ has tango/trait.Tango.html //@ hasraw sierra/struct.Sierra.html 'Tango' //@ hasraw trait.impl/tango/trait.Tango.js 'struct.Sierra.html' -//@ hasraw search-index.js 'Tango' -//@ hasraw search-index.js 'Sierra' -//@ hasraw search-index.js 'Quebec' +//@ hasraw search.index/name/*.js 'Tango' +//@ hasraw search.index/name/*.js 'Sierra' +//@ hasraw search.index/name/*.js 'Quebec' // We can use read-write to emulate the default behavior of rustdoc, when // --merge is left out. diff --git a/tests/rustdoc/merge-cross-crate-info/transitive-no-info/sierra.rs b/tests/rustdoc/merge-cross-crate-info/transitive-no-info/sierra.rs index 3eb2cebd74363..9eaa627419bb4 100644 --- a/tests/rustdoc/merge-cross-crate-info/transitive-no-info/sierra.rs +++ b/tests/rustdoc/merge-cross-crate-info/transitive-no-info/sierra.rs @@ -9,7 +9,7 @@ //@ has tango/trait.Tango.html //@ hasraw sierra/struct.Sierra.html 'Tango' //@ !has trait.impl/tango/trait.Tango.js -//@ !has search-index.js +//@ !has search.index/name/*.js // --merge=none on all crates does not generate any cross-crate info extern crate tango; diff --git a/tests/rustdoc/merge-cross-crate-info/two-separate-out-dir/echo.rs b/tests/rustdoc/merge-cross-crate-info/two-separate-out-dir/echo.rs index ee2b646e43cda..d79302e62cdc3 100644 --- a/tests/rustdoc/merge-cross-crate-info/two-separate-out-dir/echo.rs +++ b/tests/rustdoc/merge-cross-crate-info/two-separate-out-dir/echo.rs @@ -6,8 +6,8 @@ //@ has echo/enum.Echo.html //@ hasraw echo/enum.Echo.html 'Foxtrot' //@ hasraw trait.impl/foxtrot/trait.Foxtrot.js 'enum.Echo.html' -//@ hasraw search-index.js 'Foxtrot' -//@ hasraw search-index.js 'Echo' +//@ hasraw search.index/name/*.js 'Foxtrot' +//@ hasraw search.index/name/*.js 'Echo' // document two crates in different places, and merge their docs after // they are generated diff --git a/tests/rustdoc/no-unit-struct-field.rs b/tests/rustdoc/no-unit-struct-field.rs index 6ac44037ceaf8..cb74a9d19ad5b 100644 --- a/tests/rustdoc/no-unit-struct-field.rs +++ b/tests/rustdoc/no-unit-struct-field.rs @@ -1,10 +1,11 @@ // This test ensures that the tuple struct fields are not generated in the // search index. -//@ !hasraw search-index.js '"0"' -//@ !hasraw search-index.js '"1"' -//@ hasraw search-index.js '"foo_a"' -//@ hasraw search-index.js '"bar_a"' +// vlqhex encoding ` = 0, a = 1, e = 5 +//@ !hasraw search.index/name/*.js 'a0' +//@ !hasraw search.index/name/*.js 'a1' +//@ hasraw search.index/name/*.js 'efoo_a' +//@ hasraw search.index/name/*.js 'ebar_a' pub struct Bar(pub u32, pub u8); pub struct Foo { diff --git a/tests/rustdoc/primitive/search-index-primitive-inherent-method-23511.rs b/tests/rustdoc/primitive/search-index-primitive-inherent-method-23511.rs index 6054d8f12f542..ea828e08d82cc 100644 --- a/tests/rustdoc/primitive/search-index-primitive-inherent-method-23511.rs +++ b/tests/rustdoc/primitive/search-index-primitive-inherent-method-23511.rs @@ -9,7 +9,7 @@ pub mod str { #![rustc_doc_primitive = "str"] impl str { - //@ hasraw search-index.js foo + //@ hasraw search.index/name/*.js foo #[rustc_allow_incoherent_impl] pub fn foo(&self) {} } diff --git a/tests/rustdoc/search-index-summaries.rs b/tests/rustdoc/search-index-summaries.rs index 55db04340a68a..e680a9d57c11e 100644 --- a/tests/rustdoc/search-index-summaries.rs +++ b/tests/rustdoc/search-index-summaries.rs @@ -1,6 +1,6 @@ #![crate_name = "foo"] -//@ hasraw 'search.desc/foo/foo-desc-0-.js' 'Foo short link.' +//@ hasraw 'search.index/desc/*.js' 'Foo short link.' //@ !hasraw - 'www.example.com' //@ !hasraw - 'More Foo.' diff --git a/tests/rustdoc/search-index.rs b/tests/rustdoc/search-index.rs index f53862ede3800..49ccacd0a5c9f 100644 --- a/tests/rustdoc/search-index.rs +++ b/tests/rustdoc/search-index.rs @@ -2,7 +2,7 @@ use std::ops::Deref; -//@ hasraw search-index.js Foo +//@ hasraw search.index/name/*.js Foo pub use private::Foo; mod private { @@ -20,7 +20,7 @@ mod private { pub struct Bar; impl Deref for Bar { - //@ !hasraw search-index.js Target + //@ !hasraw search.index/name/*.js Target type Target = Bar; fn deref(&self) -> &Bar { self } }