2014-05-13 23:08:21 +02:00
|
|
|
/**
|
|
|
|
* @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;
|
2017-04-20 01:10:48 +02:00
|
|
|
|
|
|
|
if (this.lastValue !== null) {
|
|
|
|
this.matchResults(this.lastValue);
|
|
|
|
}
|
|
|
|
|
2014-05-13 23:08:21 +02:00
|
|
|
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();
|
2014-06-23 19:35:39 +02:00
|
|
|
search = search.replace(' ', '');
|
2014-05-13 23:08:21 +02:00
|
|
|
|
|
|
|
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 [];
|
|
|
|
}
|
|
|
|
|
Locate File: allow to search './path/to/something.txt'
Summary:
Before this change, if you search a file using Locate File,
this was the only accepted syntax for a full-path:
path/to/something.txt
After this change, some relative/absolute Unix-like variants
are also accepted:
./path/to/something.txt
/path/to/something.txt
Similar prefixes can be frequent when you quickly copy-paste things
from shell commands like 'grep' or 'find' etc.
Ref T15508
Test Plan:
Visit a lovely repository like Phorge and use Locate File with these:
1. src/applications/maniphest/editor/ManiphestEditEngine.php
2. /src/applications/maniphest/editor/ManiphestEditEngine.php
3. ./src/applications/maniphest/editor/ManiphestEditEngine.php
After this change, also 2. and 3. return the expected file.
Reviewers: O1 Blessed Committers, avivey
Reviewed By: O1 Blessed Committers, avivey
Subscribers: avivey, speck, tobiaswiese, Matthew, Cigaryno
Maniphest Tasks: T15508
Differential Revision: https://we.phorge.it/D25320
2023-07-04 17:10:19 +02:00
|
|
|
// Be nice with terminal users that may have "./" or "/" prefixes.
|
|
|
|
// Otherwise, not a single result is returned.
|
|
|
|
search = search.replace(/^\.?\//, '');
|
|
|
|
|
2014-05-13 23:08:21 +02:00
|
|
|
// 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;
|
|
|
|
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
|
2017-10-09 19:48:01 +02:00
|
|
|
* query it contains contiguously.
|
2014-05-13 23:08:21 +02:00
|
|
|
*/
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
});
|