1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-12-30 17:30:59 +01:00
phorge-phorge/webroot/rsrc/js/application/diffusion/DiffusionLocateFileSource.js
epriestley 436f0563e8 Add a SublimeText-style repository typeahead
Summary:
Allows you to quickly search for files within a repository. Roughly:

  - We build a big tree of everything and ship it to the client.
  - The client implements a bunch of Sublime-ish magic to find paths.

Test Plan: {F154007}

Reviewers: chad, btrahan

Reviewed By: btrahan

Subscribers: epriestley, zeeg

Differential Revision: https://secure.phabricator.com/D9087
2014-05-13 14:08:21 -07:00

289 lines
6.7 KiB
JavaScript

/**
* @provides javelin-diffusion-locate-file-source
* @requires javelin-install
* javelin-dom
* javelin-typeahead-preloaded-source
* javelin-util
* @javelin
*/
JX.install('DiffusionLocateFileSource', {
extend: 'TypeaheadPreloadedSource',
construct: function(uri) {
JX.TypeaheadPreloadedSource.call(this, uri);
this.cache = {};
},
members: {
tree: null,
limit: 20,
cache: null,
ondata: function(results) {
this.tree = results.tree;
this.setReady(true);
},
/**
* Match a query and show results in the typeahead.
*/
matchResults: function(value, partial) {
// For now, just pretend spaces don't exist.
var search = value.toLowerCase();
search = search.replace(" ", "");
var paths = this.findResults(search);
var nodes = [];
for (var ii = 0; ii < paths.length; ii++) {
var path = paths[ii];
var name = [];
name.push(path.path.substr(0, path.pos));
name.push(
JX.$N('strong', {}, path.path.substr(path.pos, path.score)));
var pos = path.score;
var lower = path.path.toLowerCase();
for (var jj = path.pos + path.score; jj < path.path.length; jj++) {
if (lower.charAt(jj) == search.charAt(pos)) {
pos++;
name.push(JX.$N('strong', {}, path.path.charAt(jj)));
if (pos == search.length) {
break;
}
} else {
name.push(path.path.charAt(jj));
}
}
if (jj < path.path.length - 1 ) {
name.push(path.path.substr(jj + 1));
}
var attr = {
className: 'visual-only phui-icon-view phui-font-fa fa-file'
};
var icon = JX.$N('span', attr, '');
nodes.push(
JX.$N(
'a',
{
sigil: 'typeahead-result',
className: 'jx-result diffusion-locate-file',
ref: path.path
},
[icon, name]));
}
this.invoke('resultsready', nodes, value);
if (!partial) {
this.invoke('complete');
}
},
/**
* Find the results matching a query.
*/
findResults: function(search) {
if (!search.length) {
return [];
}
// We know that the results for "abc" are always a subset of the results
// for "a" and "ab" -- and there's a good chance we already computed
// those result sets. Find the longest cached result which is a prefix
// of the search query.
var best = 0;
var start = this.tree;
for (var k in this.cache) {
if ((k.length <= search.length) &&
(k.length > best) &&
(search.substr(0, k.length) == k)) {
best = k.length;
start = this.cache[k];
}
}
var matches;
if (start === null) {
matches = null;
} else {
matches = this.matchTree(start, search, 0);
}
// Save this tree in cache; throw the cache away after a few minutes.
if (!(search in this.cache)) {
this.cache[search] = matches;
setTimeout(
JX.bind(this, function() { delete this.cache[search]; }),
1000 * 60 * 5);
}
if (!matches) {
return [];
}
var paths = [];
this.buildPaths(matches, paths, '', search, []);
paths.sort(
function(u, v) {
if (u.score != v.score) {
return (v.score - u.score);
}
if (u.pos != v.pos) {
return (u.pos - v.pos);
}
return ((u.path > v.path) ? 1 : -1);
});
var num = Math.min(paths.length, this.limit);
var results = [];
for (var ii = 0; ii < num; ii++) {
results.push(paths[ii]);
}
return results;
},
/**
* Select the subtree that matches a query.
*/
matchTree: function(tree, value, pos) {
var matches = null;
var count = 0;
for (var k in tree) {
var p = pos;
if (p != value.length) {
p = this.matchString(k, value, pos);
}
var result;
if (p == value.length) {
result = tree[k];
} else {
if (tree == 1) {
continue;
} else {
result = this.matchTree(tree[k], value, p);
if (!result) {
continue;
}
}
}
if (!matches) {
matches = {};
}
matches[k] = result;
}
return matches;
},
/**
* Look for the needle in a string, returning how much of it was found.
*/
matchString: function(haystack, needle, pos) {
var str = haystack.toLowerCase();
var len = str.length;
for (var ii = 0; ii < len; ii++) {
if (str.charAt(ii) == needle.charAt(pos)) {
pos++;
if (pos == needle.length) {
break;
}
}
}
return pos;
},
/**
* Flatten a tree into paths.
*/
buildPaths: function(matches, paths, prefix, search) {
var first = search.charAt(0);
for (var k in matches) {
if (matches[k] == 1) {
var path = prefix + k;
var lower = path.toLowerCase();
var best = 0;
var pos = 0;
for (var jj = 0; jj < lower.length; jj++) {
if (lower.charAt(jj) != first) {
continue;
}
var score = this.scoreMatch(lower, jj, search);
if (score == -1) {
break;
}
if (score > best) {
best = score;
pos = jj;
if (best == search.length) {
break;
}
}
}
paths.push({
path: path,
score: best,
pos: pos
});
} else {
this.buildPaths(matches[k], paths, prefix + k, search);
}
}
},
/**
* Score a matching string by finding the longest prefix of the search
* query it contains continguously.
*/
scoreMatch: function(haystack, haypos, search) {
var pos = 0;
for (var ii = haypos; ii < haystack.length; ii++) {
if (haystack.charAt(ii) == search.charAt(pos)) {
pos++;
if (pos == search.length) {
return pos;
}
} else {
ii++;
break;
}
}
var rem = pos;
for (/* keep going */; ii < haystack.length; ii++) {
if (haystack.charAt(ii) == search.charAt(rem)) {
rem++;
if (rem == search.length) {
return pos;
}
}
}
return -1;
}
}
});