Page MenuHomePhabricator

D9087.diff
No OneTemporary

D9087.diff

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

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)

Event Timeline