1
Fork 0

rustdoc: Implement cross-crate searching

A major discoverability issue with rustdoc is that all crates have their
documentation built in isolation, so it's difficult when looking at the
documentation for libstd to learn that there's a libcollections crate with a
HashMap in it.

This commit moves rustdoc a little closer to improving the multiple crate
experience. This unifies all search indexes for all crates into one file so all
pages share the same search index. This allows searching to work across crates
in the same documentation directory (as the standard distribution is currently
built).

This strategy involves updating a shared file amongst many rustdoc processes, so
I implemented a simple file locking API for handling synchronization for updates
to the shared files.

cc #12554
This commit is contained in:
Alex Crichton 2014-03-16 01:08:56 -07:00
parent d717d613e3
commit 848f7b734e
6 changed files with 382 additions and 156 deletions

View file

@ -9,7 +9,7 @@
// except according to those terms.
/*jslint browser: true, es5: true */
/*globals $: true, searchIndex: true, rootPath: true, allPaths: true */
/*globals $: true, rootPath: true, allPaths: true */
(function() {
"use strict";
@ -23,7 +23,8 @@
map(function(s) {
var pair = s.split("=");
params[decodeURIComponent(pair[0])] =
typeof pair[1] === "undefined" ? null : decodeURIComponent(pair[1]);
typeof pair[1] === "undefined" ?
null : decodeURIComponent(pair[1]);
});
return params;
}
@ -111,8 +112,9 @@
document.location.href = url;
});
function initSearch(searchIndex) {
var currentResults, index, params = getQueryStringParams();
function initSearch(rawSearchIndex) {
var currentResults, index, searchIndex;
var params = getQueryStringParams();
// Populate search bar with query string search term when provided,
// but only if the input bar is empty. This avoid the obnoxious issue
@ -126,7 +128,8 @@
* Executes the query and builds an index of results
* @param {[Object]} query [The user query]
* @param {[type]} max [The maximum results returned]
* @param {[type]} searchWords [The list of search words to query against]
* @param {[type]} searchWords [The list of search words to query
* against]
* @return {[type]} [A search index of results]
*/
function execQuery(query, max, searchWords) {
@ -148,7 +151,9 @@
// quoted values mean literal search
bb = searchWords.length;
if ((val.charAt(0) === "\"" || val.charAt(0) === "'") && val.charAt(val.length - 1) === val.charAt(0)) {
if ((val.charAt(0) === "\"" || val.charAt(0) === "'") &&
val.charAt(val.length - 1) === val.charAt(0))
{
val = val.substr(1, val.length - 2);
for (aa = 0; aa < bb; aa += 1) {
if (searchWords[aa] === val) {
@ -166,7 +171,10 @@
val = val.replace(/\_/g, "");
for (var i = 0; i < split.length; i++) {
for (aa = 0; aa < bb; aa += 1) {
if (searchWords[aa].indexOf(split[i]) > -1 || searchWords[aa].indexOf(val) > -1 || searchWords[aa].replace(/_/g, "").indexOf(val) > -1) {
if (searchWords[aa].indexOf(split[i]) > -1 ||
searchWords[aa].indexOf(val) > -1 ||
searchWords[aa].replace(/_/g, "").indexOf(val) > -1)
{
// filter type: ... queries
if (!typeFilter || typeFilter === searchIndex[aa].ty) {
results.push([aa, searchWords[aa].replace(/_/g, "").indexOf(val)]);
@ -185,6 +193,7 @@
results[aa].push(searchIndex[results[aa][0]].path);
results[aa].push(searchIndex[results[aa][0]].name);
results[aa].push(searchIndex[results[aa][0]].parent);
results[aa].push(searchIndex[results[aa][0]].crate);
}
// if there are no results then return to default and fail
if (results.length === 0) {
@ -193,7 +202,8 @@
// sort by exact match
results.sort(function search_complete_sort0(aaa, bbb) {
if (searchWords[aaa[0]] === valLower && searchWords[bbb[0]] !== valLower) {
if (searchWords[aaa[0]] === valLower &&
searchWords[bbb[0]] !== valLower) {
return 1;
}
});
@ -207,7 +217,8 @@
// second sorting attempt
// sort by item name
results.sort(function search_complete_sort1(aaa, bbb) {
if (searchWords[aaa[0]].length === searchWords[bbb[0]].length && searchWords[aaa[0]] > searchWords[bbb[0]]) {
if (searchWords[aaa[0]].length === searchWords[bbb[0]].length &&
searchWords[aaa[0]] > searchWords[bbb[0]]) {
return 1;
}
});
@ -223,21 +234,26 @@
// fourth sorting attempt
// sort by type
results.sort(function search_complete_sort3(aaa, bbb) {
if (searchWords[aaa[0]] === searchWords[bbb[0]] && aaa[2] > bbb[2]) {
if (searchWords[aaa[0]] === searchWords[bbb[0]] &&
aaa[2] > bbb[2]) {
return 1;
}
});
// fifth sorting attempt
// sort by path
results.sort(function search_complete_sort4(aaa, bbb) {
if (searchWords[aaa[0]] === searchWords[bbb[0]] && aaa[2] === bbb[2] && aaa[3] > bbb[3]) {
if (searchWords[aaa[0]] === searchWords[bbb[0]] &&
aaa[2] === bbb[2] && aaa[3] > bbb[3]) {
return 1;
}
});
// sixth sorting attempt
// remove duplicates, according to the data provided
for (aa = results.length - 1; aa > 0; aa -= 1) {
if (searchWords[results[aa][0]] === searchWords[results[aa - 1][0]] && results[aa][2] === results[aa - 1][2] && results[aa][3] === results[aa - 1][3]) {
if (searchWords[results[aa][0]] === searchWords[results[aa - 1][0]] &&
results[aa][2] === results[aa - 1][2] &&
results[aa][3] === results[aa - 1][3])
{
results[aa][0] = -1;
}
}
@ -245,7 +261,7 @@
var result = results[i],
name = result[4].toLowerCase(),
path = result[3].toLowerCase(),
parent = allPaths[result[5]];
parent = allPaths[result[6]][result[5]];
var valid = validateResult(name, path, split, parent);
if (!valid) {
@ -256,11 +272,14 @@
}
/**
* Validate performs the following boolean logic. For example: "File::open" will give
* IF A PARENT EXISTS => ("file" && "open") exists in (name || path || parent)
* OR => ("file" && "open") exists in (name || path )
* Validate performs the following boolean logic. For example:
* "File::open" will give IF A PARENT EXISTS => ("file" && "open")
* exists in (name || path || parent) OR => ("file" && "open") exists in
* (name || path )
*
* This could be written functionally, but I wanted to minimise
* functions on stack.
*
* This could be written functionally, but I wanted to minimise functions on stack.
* @param {[string]} name [The name of the result]
* @param {[string]} path [The path of the result]
* @param {[string]} keys [The keys to be used (["file", "open"])]
@ -273,8 +292,13 @@
//if there is a parent, then validate against parent
if (parent !== undefined) {
for (var i = 0; i < keys.length; i++) {
// if previous keys are valid and current key is in the path, name or parent
if ((validate) && (name.toLowerCase().indexOf(keys[i]) > -1 || path.toLowerCase().indexOf(keys[i]) > -1 || parent.name.toLowerCase().indexOf(keys[i]) > -1)) {
// if previous keys are valid and current key is in the
// path, name or parent
if ((validate) &&
(name.toLowerCase().indexOf(keys[i]) > -1 ||
path.toLowerCase().indexOf(keys[i]) > -1 ||
parent.name.toLowerCase().indexOf(keys[i]) > -1))
{
validate = true;
} else {
validate = false;
@ -282,8 +306,12 @@
}
} else {
for (var i = 0; i < keys.length; i++) {
// if previous keys are valid and current key is in the path, name
if ((validate) && (name.toLowerCase().indexOf(keys[i]) > -1 || path.toLowerCase().indexOf(keys[i]) > -1)) {
// if previous keys are valid and current key is in the
// path, name
if ((validate) &&
(name.toLowerCase().indexOf(keys[i]) > -1 ||
path.toLowerCase().indexOf(keys[i]) > -1))
{
validate = true;
} else {
validate = false;
@ -298,7 +326,10 @@
matches = query.match(/^(fn|mod|str(uct)?|enum|trait|t(ype)?d(ef)?)\s*:\s*/i);
if (matches) {
type = matches[1].replace(/^td$/, 'typedef').replace(/^str$/, 'struct').replace(/^tdef$/, 'typedef').replace(/^typed$/, 'typedef');
type = matches[1].replace(/^td$/, 'typedef')
.replace(/^str$/, 'struct')
.replace(/^tdef$/, 'typedef')
.replace(/^typed$/, 'typedef');
query = query.substring(matches[0].length);
}
@ -314,7 +345,6 @@
$results.on('click', function() {
var dst = $(this).find('a')[0];
console.log(window.location.pathname, dst.pathname);
if (window.location.pathname == dst.pathname) {
$('#search').addClass('hidden');
$('#main').removeClass('hidden');
@ -362,7 +392,8 @@
var output, shown, query = getQuery();
currentResults = query.id;
output = '<h1>Results for ' + query.query + (query.type ? ' (type: ' + query.type + ')' : '') + '</h1>';
output = '<h1>Results for ' + query.query +
(query.type ? ' (type: ' + query.type + ')' : '') + '</h1>';
output += '<table class="search-results">';
if (results.length > 0) {
@ -394,7 +425,7 @@
'/index.html" class="' + type +
'">' + name + '</a>';
} else if (item.parent !== undefined) {
var myparent = allPaths[item.parent];
var myparent = allPaths[item.crate][item.parent];
var anchor = '#' + type + '.' + name;
output += item.path + '::' + myparent.name +
'::<a href="' + rootPath +
@ -449,13 +480,15 @@
return;
}
// Because searching is incremental by character, only the most recent search query
// is added to the browser history.
// Because searching is incremental by character, only the most
// recent search query is added to the browser history.
if (browserSupportsHistoryApi()) {
if (!history.state && !params.search) {
history.pushState(query, "", "?search=" + encodeURIComponent(query.query));
history.pushState(query, "", "?search=" +
encodeURIComponent(query.query));
} else {
history.replaceState(query, "", "?search=" + encodeURIComponent(query.query));
history.replaceState(query, "", "?search=" +
encodeURIComponent(query.query));
}
}
@ -472,91 +505,33 @@
}
}
// TODO add sorting capability through this function?
//
// // the handler for the table heading filtering
// filterdraw = function search_complete_filterdraw(node) {
// var name = "",
// arrow = "",
// op = 0,
// tbody = node.parentNode.parentNode.nextSibling,
// anchora = {},
// tra = {},
// tha = {},
// td1a = {},
// td2a = {},
// td3a = {},
// aaa = 0,
// bbb = 0;
//
// // the 4 following conditions set the rules for each
// // table heading
// if (node === ths[0]) {
// op = 0;
// name = "name";
// ths[1].innerHTML = ths[1].innerHTML.split(" ")[0];
// ths[2].innerHTML = ths[2].innerHTML.split(" ")[0];
// ths[3].innerHTML = ths[3].innerHTML.split(" ")[0];
// }
// if (node === ths[1]) {
// op = 1;
// name = "type";
// ths[0].innerHTML = ths[0].innerHTML.split(" ")[0];
// ths[2].innerHTML = ths[2].innerHTML.split(" ")[0];
// ths[3].innerHTML = ths[3].innerHTML.split(" ")[0];
// }
// if (node === ths[2]) {
// op = 2;
// name = "path";
// ths[0].innerHTML = ths[0].innerHTML.split(" ")[0];
// ths[1].innerHTML = ths[1].innerHTML.split(" ")[0];
// ths[3].innerHTML = ths[3].innerHTML.split(" ")[0];
// }
// if (node === ths[3]) {
// op = 3;
// name = "description";
// ths[0].innerHTML = ths[0].innerHTML.split(" ")[0];
// ths[1].innerHTML = ths[1].innerHTML.split(" ")[0];
// ths[2].innerHTML = ths[2].innerHTML.split(" ")[0];
// }
//
// // ascending or descending search
// arrow = node.innerHTML.split(" ")[1];
// if (arrow === undefined || arrow === "\u25b2") {
// arrow = "\u25bc";
// } else {
// arrow = "\u25b2";
// }
//
// // filter the data
// filterdata.sort(function search_complete_filterDraw_sort(xx, yy) {
// if ((arrow === "\u25b2" && xx[op].toLowerCase() < yy[op].toLowerCase()) || (arrow === "\u25bc" && xx[op].toLowerCase() > yy[op].toLowerCase())) {
// return 1;
// }
// });
// };
showResults(results);
}
function buildIndex(searchIndex) {
var len = searchIndex.length,
i = 0,
searchWords = [];
function buildIndex(rawSearchIndex) {
searchIndex = [];
var searchWords = [];
for (var crate in rawSearchIndex) {
if (!rawSearchIndex.hasOwnProperty(crate)) { continue }
var len = rawSearchIndex[crate].length;
var i = 0;
// before any analysis is performed lets 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
for (i = 0; i < len; i += 1) {
if (typeof searchIndex[i].name === "string") {
searchWords.push(searchIndex[i].name.toLowerCase());
} else {
searchWords.push("");
// before any analysis is performed lets 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
for (i = 0; i < len; i += 1) {
rawSearchIndex[crate][i].crate = crate;
searchIndex.push(rawSearchIndex[crate][i]);
if (typeof rawSearchIndex[crate][i].name === "string") {
var word = rawSearchIndex[crate][i].name.toLowerCase();
searchWords.push(word);
} else {
searchWords.push("");
}
}
}
return searchWords;
}
@ -567,17 +542,21 @@
clearTimeout(keyUpTimeout);
keyUpTimeout = setTimeout(search, 100);
});
// Push and pop states are used to add search results to the browser history.
// Push and pop states are used to add search results to the browser
// history.
if (browserSupportsHistoryApi()) {
$(window).on('popstate', function(e) {
var params = getQueryStringParams();
// When browsing back from search results the main page visibility must be reset.
// When browsing back from search results the main page
// visibility must be reset.
if (!params.search) {
$('#main.content').removeClass('hidden');
$('#search.content').addClass('hidden');
}
// When browsing forward to search results the previous search will be repeated,
// so the currentResults are cleared to ensure the search is successful.
// 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, but don't empty the bar if there's
@ -585,19 +564,46 @@
if (params.search !== undefined) {
$('.search-input').val(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.
// 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.
search();
});
}
search();
}
index = buildIndex(searchIndex);
index = buildIndex(rawSearchIndex);
startSearch();
// Draw a convenient sidebar of known crates if we have a listing
if (rootPath == '../') {
console.log('here');
var sidebar = $('.sidebar');
var div = $('<div>').attr('class', 'block crate');
div.append($('<h2>').text('Crates'));
var crates = [];
for (var crate in rawSearchIndex) {
if (!rawSearchIndex.hasOwnProperty(crate)) { continue }
crates.push(crate);
}
crates.sort();
for (var i = 0; i < crates.length; i++) {
var klass = 'crate';
if (crates[i] == window.currentCrate) {
klass += ' current';
}
div.append($('<a>', {'href': '../' + crates[i] + '/index.html',
'class': klass}).text(crates[i]));
div.append($('<br>'));
}
sidebar.append(div);
}
}
initSearch(searchIndex);
window.initSearch = initSearch;
}());