Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F14460949
D9087.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
19 KB
Referenced Files
None
Subscribers
None
D9087.diff
View Options
diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -51,6 +51,7 @@
'AphrontFormTextWithSubmitControl' => 'view/form/control/AphrontFormTextWithSubmitControl.php',
'AphrontFormToggleButtonsControl' => 'view/form/control/AphrontFormToggleButtonsControl.php',
'AphrontFormTokenizerControl' => 'view/form/control/AphrontFormTokenizerControl.php',
+ 'AphrontFormTypeaheadControl' => 'view/form/control/AphrontFormTypeaheadControl.php',
'AphrontFormView' => 'view/form/AphrontFormView.php',
'AphrontGlyphBarView' => 'view/widget/bars/AphrontGlyphBarView.php',
'AphrontHTMLResponse' => 'aphront/response/AphrontHTMLResponse.php',
@@ -524,6 +525,7 @@
'DiffusionPathIDQuery' => 'applications/diffusion/query/pathid/DiffusionPathIDQuery.php',
'DiffusionPathQuery' => 'applications/diffusion/query/DiffusionPathQuery.php',
'DiffusionPathQueryTestCase' => 'applications/diffusion/query/pathid/__tests__/DiffusionPathQueryTestCase.php',
+ 'DiffusionPathTreeController' => 'applications/diffusion/controller/DiffusionPathTreeController.php',
'DiffusionPathValidateController' => 'applications/diffusion/controller/DiffusionPathValidateController.php',
'DiffusionPushEventViewController' => 'applications/diffusion/controller/DiffusionPushEventViewController.php',
'DiffusionPushLogController' => 'applications/diffusion/controller/DiffusionPushLogController.php',
@@ -2704,6 +2706,7 @@
'AphrontFormTextWithSubmitControl' => 'AphrontFormControl',
'AphrontFormToggleButtonsControl' => 'AphrontFormControl',
'AphrontFormTokenizerControl' => 'AphrontFormControl',
+ 'AphrontFormTypeaheadControl' => 'AphrontFormControl',
'AphrontFormView' => 'AphrontView',
'AphrontGlyphBarView' => 'AphrontBarView',
'AphrontHTMLResponse' => 'AphrontResponse',
@@ -3164,6 +3167,7 @@
'DiffusionMirrorEditController' => 'DiffusionController',
'DiffusionPathCompleteController' => 'DiffusionController',
'DiffusionPathQueryTestCase' => 'PhabricatorTestCase',
+ 'DiffusionPathTreeController' => 'DiffusionController',
'DiffusionPathValidateController' => 'DiffusionController',
'DiffusionPushEventViewController' => 'DiffusionPushLogController',
'DiffusionPushLogController' => 'DiffusionController',
diff --git a/src/applications/diffusion/application/PhabricatorApplicationDiffusion.php b/src/applications/diffusion/application/PhabricatorApplicationDiffusion.php
--- a/src/applications/diffusion/application/PhabricatorApplicationDiffusion.php
+++ b/src/applications/diffusion/application/PhabricatorApplicationDiffusion.php
@@ -85,6 +85,7 @@
'hosting/' => 'DiffusionRepositoryEditHostingController',
'(?P<serve>serve)/' => 'DiffusionRepositoryEditHostingController',
),
+ 'pathtree/(?P<dblob>.*)' => 'DiffusionPathTreeController',
'mirror/' => array(
'edit/(?:(?P<id>\d+)/)?' => 'DiffusionMirrorEditController',
'delete/(?P<id>\d+)/' => 'DiffusionMirrorDeleteController',
diff --git a/src/applications/diffusion/conduit/ConduitAPI_diffusion_querypaths_Method.php b/src/applications/diffusion/conduit/ConduitAPI_diffusion_querypaths_Method.php
--- a/src/applications/diffusion/conduit/ConduitAPI_diffusion_querypaths_Method.php
+++ b/src/applications/diffusion/conduit/ConduitAPI_diffusion_querypaths_Method.php
@@ -15,7 +15,7 @@
return array(
'path' => 'required string',
'commit' => 'required string',
- 'pattern' => 'required string',
+ 'pattern' => 'optional string',
'limit' => 'optional int',
'offset' => 'optional int',
);
@@ -40,6 +40,7 @@
$commit,
$path);
+
$lines = id(new LinesOfALargeExecFuture($future))->setDelimiter("\0");
return $this->filterResults($lines, $request);
}
@@ -65,23 +66,35 @@
$lines[] = $path;
}
}
+
return $this->filterResults($lines, $request);
}
protected function filterResults($lines, ConduitAPIRequest $request) {
$pattern = $request->getValue('pattern');
- $limit = $request->getValue('limit');
- $offset = $request->getValue('offset');
+ $limit = (int)$request->getValue('limit');
+ $offset = (int)$request->getValue('offset');
+
+ if (strlen($pattern)) {
+ $pattern = '/'.preg_quote($pattern, '/').'/';
+ }
$results = array();
+ $count = 0;
foreach ($lines as $line) {
- if (preg_match('#'.str_replace('#', '\#', $pattern).'#', $line)) {
- $results[] = $line;
- if (count($results) >= $offset + $limit) {
+ if (!$pattern || preg_match($pattern, $line)) {
+ if ($count >= $offset) {
+ $results[] = $line;
+ }
+
+ $count++;
+
+ if ($limit && ($count >= ($offset + $limit))) {
break;
}
}
}
+
return $results;
}
}
diff --git a/src/applications/diffusion/controller/DiffusionPathTreeController.php b/src/applications/diffusion/controller/DiffusionPathTreeController.php
new file mode 100644
--- /dev/null
+++ b/src/applications/diffusion/controller/DiffusionPathTreeController.php
@@ -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));
+ }
+}
diff --git a/src/applications/diffusion/controller/DiffusionRepositoryController.php b/src/applications/diffusion/controller/DiffusionRepositoryController.php
--- a/src/applications/diffusion/controller/DiffusionRepositoryController.php
+++ b/src/applications/diffusion/controller/DiffusionRepositoryController.php
@@ -366,9 +366,9 @@
$button->setTag('a');
$button->setIcon($icon);
$button->setHref($drequest->generateURI(
- array(
- 'action' => 'tags',
- )));
+ array(
+ 'action' => 'tags',
+ )));
$header->addActionLink($button);
@@ -529,6 +529,33 @@
$header->addActionLink($button);
$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);
return $browse_panel;
diff --git a/src/applications/diffusion/request/DiffusionRequest.php b/src/applications/diffusion/request/DiffusionRequest.php
--- a/src/applications/diffusion/request/DiffusionRequest.php
+++ b/src/applications/diffusion/request/DiffusionRequest.php
@@ -520,6 +520,7 @@
case 'tags':
case 'branches':
case 'lint':
+ case 'pathtree':
$uri = "/diffusion/{$callsign}{$action}/{$path}{$commit}{$line}";
break;
case 'branch':
diff --git a/src/applications/repository/storage/PhabricatorRepository.php b/src/applications/repository/storage/PhabricatorRepository.php
--- a/src/applications/repository/storage/PhabricatorRepository.php
+++ b/src/applications/repository/storage/PhabricatorRepository.php
@@ -1191,6 +1191,10 @@
return Filesystem::isDescendant($this->getLocalPath(), $default_path);
}
+ public function canUsePathTree() {
+ return !$this->isSVN();
+ }
+
public function canMirror() {
if ($this->isGit() || $this->isHg()) {
return true;
diff --git a/src/view/form/control/AphrontFormTypeaheadControl.php b/src/view/form/control/AphrontFormTypeaheadControl.php
new file mode 100644
--- /dev/null
+++ b/src/view/form/control/AphrontFormTypeaheadControl.php
@@ -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(),
+ )));
+ }
+
+}
diff --git a/webroot/rsrc/css/aphront/table-view.css b/webroot/rsrc/css/aphront/table-view.css
--- a/webroot/rsrc/css/aphront/table-view.css
+++ b/webroot/rsrc/css/aphront/table-view.css
@@ -268,5 +268,13 @@
}
.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;
}
diff --git a/webroot/rsrc/css/aphront/typeahead.css b/webroot/rsrc/css/aphront/typeahead.css
--- a/webroot/rsrc/css/aphront/typeahead.css
+++ b/webroot/rsrc/css/aphront/typeahead.css
@@ -18,6 +18,12 @@
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 {
color: #333;
display: block;
@@ -51,3 +57,16 @@
div.jx-tokenizer-container-focused.jx-typeahead-waiting {
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;
+}
diff --git a/webroot/rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadPreloadedSource.js b/webroot/rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadPreloadedSource.js
--- a/webroot/rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadPreloadedSource.js
+++ b/webroot/rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadPreloadedSource.js
@@ -53,6 +53,10 @@
this.matchResults(this.lastValue);
}
this.ready = true;
+ },
+
+ setReady: function(ready) {
+ this.ready = ready;
}
}
});
diff --git a/webroot/rsrc/js/application/diffusion/DiffusionLocateFileSource.js b/webroot/rsrc/js/application/diffusion/DiffusionLocateFileSource.js
new file mode 100644
--- /dev/null
+++ b/webroot/rsrc/js/application/diffusion/DiffusionLocateFileSource.js
@@ -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;
+ }
+
+ }
+});
diff --git a/webroot/rsrc/js/application/diffusion/behavior-locate-file.js b/webroot/rsrc/js/application/diffusion/behavior-locate-file.js
new file mode 100644
--- /dev/null
+++ b/webroot/rsrc/js/application/diffusion/behavior-locate-file.js
@@ -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();
+ }
+ });
+
+});
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Sat, Dec 28, 8:38 AM (7 h, 4 m)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
6936328
Default Alt Text
D9087.diff (19 KB)
Attached To
Mode
D9087: Add a SublimeText-style repository typeahead
Attached
Detach File
Event Timeline
Log In to Comment