mirror of
https://we.phorge.it/source/phorge.git
synced 2024-12-29 08:50:58 +01:00
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
This commit is contained in:
parent
82102cd95a
commit
436f0563e8
13 changed files with 485 additions and 9 deletions
|
@ -51,6 +51,7 @@ phutil_register_library_map(array(
|
||||||
'AphrontFormTextWithSubmitControl' => 'view/form/control/AphrontFormTextWithSubmitControl.php',
|
'AphrontFormTextWithSubmitControl' => 'view/form/control/AphrontFormTextWithSubmitControl.php',
|
||||||
'AphrontFormToggleButtonsControl' => 'view/form/control/AphrontFormToggleButtonsControl.php',
|
'AphrontFormToggleButtonsControl' => 'view/form/control/AphrontFormToggleButtonsControl.php',
|
||||||
'AphrontFormTokenizerControl' => 'view/form/control/AphrontFormTokenizerControl.php',
|
'AphrontFormTokenizerControl' => 'view/form/control/AphrontFormTokenizerControl.php',
|
||||||
|
'AphrontFormTypeaheadControl' => 'view/form/control/AphrontFormTypeaheadControl.php',
|
||||||
'AphrontFormView' => 'view/form/AphrontFormView.php',
|
'AphrontFormView' => 'view/form/AphrontFormView.php',
|
||||||
'AphrontGlyphBarView' => 'view/widget/bars/AphrontGlyphBarView.php',
|
'AphrontGlyphBarView' => 'view/widget/bars/AphrontGlyphBarView.php',
|
||||||
'AphrontHTMLResponse' => 'aphront/response/AphrontHTMLResponse.php',
|
'AphrontHTMLResponse' => 'aphront/response/AphrontHTMLResponse.php',
|
||||||
|
@ -524,6 +525,7 @@ phutil_register_library_map(array(
|
||||||
'DiffusionPathIDQuery' => 'applications/diffusion/query/pathid/DiffusionPathIDQuery.php',
|
'DiffusionPathIDQuery' => 'applications/diffusion/query/pathid/DiffusionPathIDQuery.php',
|
||||||
'DiffusionPathQuery' => 'applications/diffusion/query/DiffusionPathQuery.php',
|
'DiffusionPathQuery' => 'applications/diffusion/query/DiffusionPathQuery.php',
|
||||||
'DiffusionPathQueryTestCase' => 'applications/diffusion/query/pathid/__tests__/DiffusionPathQueryTestCase.php',
|
'DiffusionPathQueryTestCase' => 'applications/diffusion/query/pathid/__tests__/DiffusionPathQueryTestCase.php',
|
||||||
|
'DiffusionPathTreeController' => 'applications/diffusion/controller/DiffusionPathTreeController.php',
|
||||||
'DiffusionPathValidateController' => 'applications/diffusion/controller/DiffusionPathValidateController.php',
|
'DiffusionPathValidateController' => 'applications/diffusion/controller/DiffusionPathValidateController.php',
|
||||||
'DiffusionPushEventViewController' => 'applications/diffusion/controller/DiffusionPushEventViewController.php',
|
'DiffusionPushEventViewController' => 'applications/diffusion/controller/DiffusionPushEventViewController.php',
|
||||||
'DiffusionPushLogController' => 'applications/diffusion/controller/DiffusionPushLogController.php',
|
'DiffusionPushLogController' => 'applications/diffusion/controller/DiffusionPushLogController.php',
|
||||||
|
@ -2704,6 +2706,7 @@ phutil_register_library_map(array(
|
||||||
'AphrontFormTextWithSubmitControl' => 'AphrontFormControl',
|
'AphrontFormTextWithSubmitControl' => 'AphrontFormControl',
|
||||||
'AphrontFormToggleButtonsControl' => 'AphrontFormControl',
|
'AphrontFormToggleButtonsControl' => 'AphrontFormControl',
|
||||||
'AphrontFormTokenizerControl' => 'AphrontFormControl',
|
'AphrontFormTokenizerControl' => 'AphrontFormControl',
|
||||||
|
'AphrontFormTypeaheadControl' => 'AphrontFormControl',
|
||||||
'AphrontFormView' => 'AphrontView',
|
'AphrontFormView' => 'AphrontView',
|
||||||
'AphrontGlyphBarView' => 'AphrontBarView',
|
'AphrontGlyphBarView' => 'AphrontBarView',
|
||||||
'AphrontHTMLResponse' => 'AphrontResponse',
|
'AphrontHTMLResponse' => 'AphrontResponse',
|
||||||
|
@ -3164,6 +3167,7 @@ phutil_register_library_map(array(
|
||||||
'DiffusionMirrorEditController' => 'DiffusionController',
|
'DiffusionMirrorEditController' => 'DiffusionController',
|
||||||
'DiffusionPathCompleteController' => 'DiffusionController',
|
'DiffusionPathCompleteController' => 'DiffusionController',
|
||||||
'DiffusionPathQueryTestCase' => 'PhabricatorTestCase',
|
'DiffusionPathQueryTestCase' => 'PhabricatorTestCase',
|
||||||
|
'DiffusionPathTreeController' => 'DiffusionController',
|
||||||
'DiffusionPathValidateController' => 'DiffusionController',
|
'DiffusionPathValidateController' => 'DiffusionController',
|
||||||
'DiffusionPushEventViewController' => 'DiffusionPushLogController',
|
'DiffusionPushEventViewController' => 'DiffusionPushLogController',
|
||||||
'DiffusionPushLogController' => 'DiffusionController',
|
'DiffusionPushLogController' => 'DiffusionController',
|
||||||
|
|
|
@ -85,6 +85,7 @@ final class PhabricatorApplicationDiffusion extends PhabricatorApplication {
|
||||||
'hosting/' => 'DiffusionRepositoryEditHostingController',
|
'hosting/' => 'DiffusionRepositoryEditHostingController',
|
||||||
'(?P<serve>serve)/' => 'DiffusionRepositoryEditHostingController',
|
'(?P<serve>serve)/' => 'DiffusionRepositoryEditHostingController',
|
||||||
),
|
),
|
||||||
|
'pathtree/(?P<dblob>.*)' => 'DiffusionPathTreeController',
|
||||||
'mirror/' => array(
|
'mirror/' => array(
|
||||||
'edit/(?:(?P<id>\d+)/)?' => 'DiffusionMirrorEditController',
|
'edit/(?:(?P<id>\d+)/)?' => 'DiffusionMirrorEditController',
|
||||||
'delete/(?P<id>\d+)/' => 'DiffusionMirrorDeleteController',
|
'delete/(?P<id>\d+)/' => 'DiffusionMirrorDeleteController',
|
||||||
|
|
|
@ -15,7 +15,7 @@ final class ConduitAPI_diffusion_querypaths_Method
|
||||||
return array(
|
return array(
|
||||||
'path' => 'required string',
|
'path' => 'required string',
|
||||||
'commit' => 'required string',
|
'commit' => 'required string',
|
||||||
'pattern' => 'required string',
|
'pattern' => 'optional string',
|
||||||
'limit' => 'optional int',
|
'limit' => 'optional int',
|
||||||
'offset' => 'optional int',
|
'offset' => 'optional int',
|
||||||
);
|
);
|
||||||
|
@ -40,6 +40,7 @@ final class ConduitAPI_diffusion_querypaths_Method
|
||||||
$commit,
|
$commit,
|
||||||
$path);
|
$path);
|
||||||
|
|
||||||
|
|
||||||
$lines = id(new LinesOfALargeExecFuture($future))->setDelimiter("\0");
|
$lines = id(new LinesOfALargeExecFuture($future))->setDelimiter("\0");
|
||||||
return $this->filterResults($lines, $request);
|
return $this->filterResults($lines, $request);
|
||||||
}
|
}
|
||||||
|
@ -65,23 +66,35 @@ final class ConduitAPI_diffusion_querypaths_Method
|
||||||
$lines[] = $path;
|
$lines[] = $path;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->filterResults($lines, $request);
|
return $this->filterResults($lines, $request);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function filterResults($lines, ConduitAPIRequest $request) {
|
protected function filterResults($lines, ConduitAPIRequest $request) {
|
||||||
$pattern = $request->getValue('pattern');
|
$pattern = $request->getValue('pattern');
|
||||||
$limit = $request->getValue('limit');
|
$limit = (int)$request->getValue('limit');
|
||||||
$offset = $request->getValue('offset');
|
$offset = (int)$request->getValue('offset');
|
||||||
|
|
||||||
|
if (strlen($pattern)) {
|
||||||
|
$pattern = '/'.preg_quote($pattern, '/').'/';
|
||||||
|
}
|
||||||
|
|
||||||
$results = array();
|
$results = array();
|
||||||
|
$count = 0;
|
||||||
foreach ($lines as $line) {
|
foreach ($lines as $line) {
|
||||||
if (preg_match('#'.str_replace('#', '\#', $pattern).'#', $line)) {
|
if (!$pattern || preg_match($pattern, $line)) {
|
||||||
$results[] = $line;
|
if ($count >= $offset) {
|
||||||
if (count($results) >= $offset + $limit) {
|
$results[] = $line;
|
||||||
|
}
|
||||||
|
|
||||||
|
$count++;
|
||||||
|
|
||||||
|
if ($limit && ($count >= ($offset + $limit))) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $results;
|
return $results;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class DiffusionPathTreeController extends DiffusionController {
|
||||||
|
|
||||||
|
public function processRequest() {
|
||||||
|
$drequest = $this->getDiffusionRequest();
|
||||||
|
|
||||||
|
if (!$drequest->getRepository()->canUsePathTree()) {
|
||||||
|
return new Aphront404Response();
|
||||||
|
}
|
||||||
|
|
||||||
|
$paths = $this->callConduitWithDiffusionRequest(
|
||||||
|
'diffusion.querypaths',
|
||||||
|
array(
|
||||||
|
'path' => $drequest->getPath(),
|
||||||
|
'commit' => $drequest->getCommit(),
|
||||||
|
));
|
||||||
|
|
||||||
|
$tree = array();
|
||||||
|
foreach ($paths as $path) {
|
||||||
|
$parts = preg_split('((?<=/))', $path);
|
||||||
|
$cursor = &$tree;
|
||||||
|
foreach ($parts as $part) {
|
||||||
|
if (!is_array($cursor)) {
|
||||||
|
$cursor = array();
|
||||||
|
}
|
||||||
|
if (!isset($cursor[$part])) {
|
||||||
|
$cursor[$part] = 1;
|
||||||
|
}
|
||||||
|
$cursor = &$cursor[$part];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return id(new AphrontAjaxResponse())->setContent(array('tree' => $tree));
|
||||||
|
}
|
||||||
|
}
|
|
@ -366,9 +366,9 @@ final class DiffusionRepositoryController extends DiffusionController {
|
||||||
$button->setTag('a');
|
$button->setTag('a');
|
||||||
$button->setIcon($icon);
|
$button->setIcon($icon);
|
||||||
$button->setHref($drequest->generateURI(
|
$button->setHref($drequest->generateURI(
|
||||||
array(
|
array(
|
||||||
'action' => 'tags',
|
'action' => 'tags',
|
||||||
)));
|
)));
|
||||||
|
|
||||||
$header->addActionLink($button);
|
$header->addActionLink($button);
|
||||||
|
|
||||||
|
@ -529,6 +529,33 @@ final class DiffusionRepositoryController extends DiffusionController {
|
||||||
|
|
||||||
$header->addActionLink($button);
|
$header->addActionLink($button);
|
||||||
$browse_panel->setHeader($header);
|
$browse_panel->setHeader($header);
|
||||||
|
|
||||||
|
if ($repository->canUsePathTree()) {
|
||||||
|
Javelin::initBehavior(
|
||||||
|
'diffusion-locate-file',
|
||||||
|
array(
|
||||||
|
'controlID' => 'locate-control',
|
||||||
|
'inputID' => 'locate-input',
|
||||||
|
'browseBaseURI' => (string)$drequest->generateURI(
|
||||||
|
array(
|
||||||
|
'action' => 'browse',
|
||||||
|
)),
|
||||||
|
'uri' => (string)$drequest->generateURI(
|
||||||
|
array(
|
||||||
|
'action' => 'pathtree',
|
||||||
|
)),
|
||||||
|
));
|
||||||
|
|
||||||
|
$form = id(new AphrontFormView())
|
||||||
|
->setUser($viewer)
|
||||||
|
->appendChild(
|
||||||
|
id(new AphrontFormTypeaheadControl())
|
||||||
|
->setHardpointID('locate-control')
|
||||||
|
->setID('locate-input')
|
||||||
|
->setLabel(pht('Locate File')));
|
||||||
|
$browse_panel->appendChild($form->buildLayoutView());
|
||||||
|
}
|
||||||
|
|
||||||
$browse_panel->appendChild($browse_table);
|
$browse_panel->appendChild($browse_table);
|
||||||
|
|
||||||
return $browse_panel;
|
return $browse_panel;
|
||||||
|
|
|
@ -520,6 +520,7 @@ abstract class DiffusionRequest {
|
||||||
case 'tags':
|
case 'tags':
|
||||||
case 'branches':
|
case 'branches':
|
||||||
case 'lint':
|
case 'lint':
|
||||||
|
case 'pathtree':
|
||||||
$uri = "/diffusion/{$callsign}{$action}/{$path}{$commit}{$line}";
|
$uri = "/diffusion/{$callsign}{$action}/{$path}{$commit}{$line}";
|
||||||
break;
|
break;
|
||||||
case 'branch':
|
case 'branch':
|
||||||
|
|
|
@ -1191,6 +1191,10 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
|
||||||
return Filesystem::isDescendant($this->getLocalPath(), $default_path);
|
return Filesystem::isDescendant($this->getLocalPath(), $default_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function canUsePathTree() {
|
||||||
|
return !$this->isSVN();
|
||||||
|
}
|
||||||
|
|
||||||
public function canMirror() {
|
public function canMirror() {
|
||||||
if ($this->isGit() || $this->isHg()) {
|
if ($this->isGit() || $this->isHg()) {
|
||||||
return true;
|
return true;
|
||||||
|
|
39
src/view/form/control/AphrontFormTypeaheadControl.php
Normal file
39
src/view/form/control/AphrontFormTypeaheadControl.php
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class AphrontFormTypeaheadControl extends AphrontFormControl {
|
||||||
|
|
||||||
|
private $hardpointID;
|
||||||
|
|
||||||
|
public function setHardpointID($hardpoint_id) {
|
||||||
|
$this->hardpointID = $hardpoint_id;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getHardpointID() {
|
||||||
|
return $this->hardpointID;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getCustomControlClass() {
|
||||||
|
return 'aphront-form-control-typeahead';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function renderInput() {
|
||||||
|
return javelin_tag(
|
||||||
|
'div',
|
||||||
|
array(
|
||||||
|
'style' => 'position: relative;',
|
||||||
|
'id' => $this->getHardpointID(),
|
||||||
|
),
|
||||||
|
javelin_tag(
|
||||||
|
'input',
|
||||||
|
array(
|
||||||
|
'type' => 'text',
|
||||||
|
'name' => $this->getName(),
|
||||||
|
'value' => $this->getValue(),
|
||||||
|
'disabled' => $this->getDisabled() ? 'disabled' : null,
|
||||||
|
'autocomplete' => 'off',
|
||||||
|
'id' => $this->getID(),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -268,5 +268,13 @@ span.single-display-line-content {
|
||||||
}
|
}
|
||||||
|
|
||||||
.phui-object-box .aphront-table-view {
|
.phui-object-box .aphront-table-view {
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 1px 0 0 0;
|
||||||
|
border-color: {$lightblueborder};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* When a table immediately follows a header, remove the top border. */
|
||||||
|
.phui-object-box .phui-header-shell +
|
||||||
|
.aphront-table-wrap .aphront-table-view {
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,12 @@ div.jx-typeahead-results {
|
||||||
margin: -1px 1% 0;
|
margin: -1px 1% 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.aphront-form-control-typeahead div.jx-typeahead-results {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
div.jx-typeahead-results a.jx-result {
|
div.jx-typeahead-results a.jx-result {
|
||||||
color: #333;
|
color: #333;
|
||||||
display: block;
|
display: block;
|
||||||
|
@ -51,3 +57,16 @@ input.jx-typeahead-placeholder {
|
||||||
div.jx-tokenizer-container-focused.jx-typeahead-waiting {
|
div.jx-tokenizer-container-focused.jx-typeahead-waiting {
|
||||||
border-color: {$lightblueborder};
|
border-color: {$lightblueborder};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.jx-typeahead-results a.diffusion-locate-file {
|
||||||
|
padding: 4px 8px;
|
||||||
|
color: {$darkgreytext}
|
||||||
|
}
|
||||||
|
|
||||||
|
.diffusion-locate-file strong {
|
||||||
|
color: {$blue};
|
||||||
|
}
|
||||||
|
|
||||||
|
.diffusion-locate-file .phui-icon-view {
|
||||||
|
padding-right: 8px;
|
||||||
|
}
|
||||||
|
|
|
@ -53,6 +53,10 @@ JX.install('TypeaheadPreloadedSource', {
|
||||||
this.matchResults(this.lastValue);
|
this.matchResults(this.lastValue);
|
||||||
}
|
}
|
||||||
this.ready = true;
|
this.ready = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
setReady: function(ready) {
|
||||||
|
this.ready = ready;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,289 @@
|
||||||
|
/**
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,31 @@
|
||||||
|
/**
|
||||||
|
* @provides javelin-behavior-diffusion-locate-file
|
||||||
|
* @requires javelin-behavior
|
||||||
|
* javelin-diffusion-locate-file-source
|
||||||
|
* javelin-dom
|
||||||
|
* javelin-typeahead
|
||||||
|
* javelin-uri
|
||||||
|
*/
|
||||||
|
|
||||||
|
JX.behavior('diffusion-locate-file', function(config) {
|
||||||
|
var control = JX.$(config.controlID);
|
||||||
|
var input = JX.$(config.inputID);
|
||||||
|
|
||||||
|
var datasource = new JX.DiffusionLocateFileSource(config.uri);
|
||||||
|
|
||||||
|
var typeahead = new JX.Typeahead(control, input);
|
||||||
|
typeahead.setDatasource(datasource);
|
||||||
|
|
||||||
|
typeahead.listen('choose', function(r) {
|
||||||
|
JX.$U(config.browseBaseURI + r.ref).go();
|
||||||
|
});
|
||||||
|
|
||||||
|
var started = false;
|
||||||
|
JX.DOM.listen(input, 'click', null, function() {
|
||||||
|
if (!started) {
|
||||||
|
started = true;
|
||||||
|
typeahead.start();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
Loading…
Reference in a new issue