mirror of
https://we.phorge.it/source/phorge.git
synced 2024-12-23 22:10:55 +01:00
Modularize the "jump nav" behaviors in global search
Summary: Depends on D19087. Ref T13079. This still doesn't feel like the most clean, general system in the world, but is a step forward from hard-coded `switch()` stuff. Test Plan: - Jumped to `r`. - Jumped to `a`. - Jumped to `r poe` (multiple results). - Jumped to `r poetry` (one result). - Jumped to `r syzygy` (no results). - Jumped to `p`. - Jumped to `p robot` (multiple results); `p assessment` (one result). - The behavior for `p <string>` has changed slightly but should be more powerful now (it's consistent with `r <string>`). - Jumped to `s <symbol>` and `s <context>-><symbol>`. - Jumped to `d`. - Jumped to `f`. - Jumped to `t`. - Jumped to `T123`, `D123`, `@dog`, `PHID-DREV-abcd`, etc. Maniphest Tasks: T13079 Differential Revision: https://secure.phabricator.com/D19088
This commit is contained in:
parent
abe5fd57b0
commit
4bccb1547d
9 changed files with 246 additions and 148 deletions
|
@ -3154,7 +3154,6 @@ phutil_register_library_map(array(
|
|||
'PhabricatorJSONExportFormat' => 'infrastructure/export/format/PhabricatorJSONExportFormat.php',
|
||||
'PhabricatorJavelinLinter' => 'infrastructure/lint/linter/PhabricatorJavelinLinter.php',
|
||||
'PhabricatorJiraIssueHasObjectEdgeType' => 'applications/doorkeeper/edge/PhabricatorJiraIssueHasObjectEdgeType.php',
|
||||
'PhabricatorJumpNavHandler' => 'applications/search/engine/PhabricatorJumpNavHandler.php',
|
||||
'PhabricatorKeyValueDatabaseCache' => 'applications/cache/PhabricatorKeyValueDatabaseCache.php',
|
||||
'PhabricatorKeyValueSerializingCacheProxy' => 'applications/cache/PhabricatorKeyValueSerializingCacheProxy.php',
|
||||
'PhabricatorKeyboardRemarkupRule' => 'infrastructure/markup/rule/PhabricatorKeyboardRemarkupRule.php',
|
||||
|
@ -8708,7 +8707,6 @@ phutil_register_library_map(array(
|
|||
'PhabricatorJSONExportFormat' => 'PhabricatorExportFormat',
|
||||
'PhabricatorJavelinLinter' => 'ArcanistLinter',
|
||||
'PhabricatorJiraIssueHasObjectEdgeType' => 'PhabricatorEdgeType',
|
||||
'PhabricatorJumpNavHandler' => 'Phobject',
|
||||
'PhabricatorKeyValueDatabaseCache' => 'PhutilKeyValueCache',
|
||||
'PhabricatorKeyValueSerializingCacheProxy' => 'PhutilKeyValueCacheProxy',
|
||||
'PhabricatorKeyboardRemarkupRule' => 'PhutilRemarkupRule',
|
||||
|
|
|
@ -9,4 +9,74 @@ final class DiffusionDatasourceEngineExtension
|
|||
new DiffusionSymbolDatasource(),
|
||||
);
|
||||
}
|
||||
|
||||
public function newJumpURI($query) {
|
||||
$viewer = $this->getViewer();
|
||||
|
||||
// Send "r" to Diffusion.
|
||||
if (preg_match('/^r\z/i', $query)) {
|
||||
return '/diffusion/';
|
||||
}
|
||||
|
||||
// Send "a" to the commit list ("Audit").
|
||||
if (preg_match('/^a\z/i', $query)) {
|
||||
return '/diffusion/commit/';
|
||||
}
|
||||
|
||||
// Send "r <string>" to a search for a matching repository.
|
||||
$matches = null;
|
||||
if (preg_match('/^r\s+(.+)\z/i', $query, $matches)) {
|
||||
$raw_query = $matches[1];
|
||||
|
||||
$engine = id(new PhabricatorRepository())
|
||||
->newFerretEngine();
|
||||
|
||||
$compiler = id(new PhutilSearchQueryCompiler())
|
||||
->setEnableFunctions(true);
|
||||
|
||||
$raw_tokens = $compiler->newTokens($raw_query);
|
||||
|
||||
$fulltext_tokens = array();
|
||||
foreach ($raw_tokens as $raw_token) {
|
||||
$fulltext_token = id(new PhabricatorFulltextToken())
|
||||
->setToken($raw_token);
|
||||
$fulltext_tokens[] = $fulltext_token;
|
||||
}
|
||||
|
||||
$repositories = id(new PhabricatorRepositoryQuery())
|
||||
->setViewer($viewer)
|
||||
->withFerretConstraint($engine, $fulltext_tokens)
|
||||
->execute();
|
||||
if (count($repositories) == 1) {
|
||||
// Just one match, jump to repository.
|
||||
return head($repositories)->getURI();
|
||||
} else {
|
||||
// More than one match, jump to search.
|
||||
return urisprintf(
|
||||
'/diffusion/?order=relevance&query=%s#R',
|
||||
$raw_query);
|
||||
}
|
||||
}
|
||||
|
||||
// Send "s <string>" to a symbol search.
|
||||
$matches = null;
|
||||
if (preg_match('/^s\s+(.+)\z/i', $query, $matches)) {
|
||||
$symbol = $matches[1];
|
||||
|
||||
$parts = null;
|
||||
if (preg_match('/(.*)(?:\\.|::|->)(.*)/', $symbol, $parts)) {
|
||||
return urisprintf(
|
||||
'/diffusion/symbol/%s/?jump=true&context=%s',
|
||||
$parts[2],
|
||||
$parts[1]);
|
||||
} else {
|
||||
return urisprintf(
|
||||
'/diffusion/symbol/%s/?jump=true',
|
||||
$symbol);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -8,4 +8,29 @@ final class PhabricatorPeopleDatasourceEngineExtension
|
|||
new PhabricatorPeopleDatasource(),
|
||||
);
|
||||
}
|
||||
|
||||
public function newJumpURI($query) {
|
||||
$viewer = $this->getViewer();
|
||||
|
||||
// Send "u" to the user directory.
|
||||
if (preg_match('/^u\z/i', $query)) {
|
||||
return '/people/';
|
||||
}
|
||||
|
||||
// Send "u <string>" to the user's profile page.
|
||||
$matches = null;
|
||||
if (preg_match('/^u\s+(.+)\z/i', $query, $matches)) {
|
||||
$raw_query = $matches[1];
|
||||
|
||||
// TODO: We could test that this is a valid username and jump to
|
||||
// a search in the user directory if it isn't.
|
||||
|
||||
return urisprintf(
|
||||
'/p/%s/',
|
||||
$raw_query);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -8,4 +8,50 @@ final class ProjectDatasourceEngineExtension
|
|||
new PhabricatorProjectDatasource(),
|
||||
);
|
||||
}
|
||||
|
||||
public function newJumpURI($query) {
|
||||
$viewer = $this->getViewer();
|
||||
|
||||
// Send "p" to Projects.
|
||||
if (preg_match('/^p\z/i', $query)) {
|
||||
return '/diffusion/';
|
||||
}
|
||||
|
||||
// Send "p <string>" to a search for similar projects.
|
||||
$matches = null;
|
||||
if (preg_match('/^p\s+(.+)\z/i', $query, $matches)) {
|
||||
$raw_query = $matches[1];
|
||||
|
||||
$engine = id(new PhabricatorProject())
|
||||
->newFerretEngine();
|
||||
|
||||
$compiler = id(new PhutilSearchQueryCompiler())
|
||||
->setEnableFunctions(true);
|
||||
|
||||
$raw_tokens = $compiler->newTokens($raw_query);
|
||||
|
||||
$fulltext_tokens = array();
|
||||
foreach ($raw_tokens as $raw_token) {
|
||||
$fulltext_token = id(new PhabricatorFulltextToken())
|
||||
->setToken($raw_token);
|
||||
$fulltext_tokens[] = $fulltext_token;
|
||||
}
|
||||
|
||||
$projects = id(new PhabricatorProjectQuery())
|
||||
->setViewer($viewer)
|
||||
->withFerretConstraint($engine, $fulltext_tokens)
|
||||
->execute();
|
||||
if (count($projects) == 1) {
|
||||
// Just one match, jump to project.
|
||||
return head($projects)->getURI();
|
||||
} else {
|
||||
// More than one match, jump to search.
|
||||
return urisprintf(
|
||||
'/project/?order=relevance&query=%s#R',
|
||||
$raw_query);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,13 +11,15 @@ final class PhabricatorSearchController
|
|||
|
||||
public function handleRequest(AphrontRequest $request) {
|
||||
$viewer = $this->getViewer();
|
||||
$query = $request->getStr('query');
|
||||
|
||||
if ($request->getStr('jump') != 'no') {
|
||||
$response = PhabricatorJumpNavHandler::getJumpResponse(
|
||||
$viewer,
|
||||
$request->getStr('query'));
|
||||
if ($response) {
|
||||
return $response;
|
||||
if ($request->getStr('jump') != 'no' && strlen($query)) {
|
||||
$jump_uri = id(new PhabricatorDatasourceEngine())
|
||||
->setViewer($viewer)
|
||||
->newJumpURI($query);
|
||||
|
||||
if ($jump_uri !== null) {
|
||||
return id(new AphrontRedirectResponse())->setURI($jump_uri);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -29,7 +31,7 @@ final class PhabricatorSearchController
|
|||
if ($request->getBool('search:primary')) {
|
||||
|
||||
// If there's no query, just take the user to advanced search.
|
||||
if (!strlen($request->getStr('query'))) {
|
||||
if (!strlen($query)) {
|
||||
$advanced_uri = '/search/query/advanced/';
|
||||
return id(new AphrontRedirectResponse())->setURI($advanced_uri);
|
||||
}
|
||||
|
@ -71,7 +73,7 @@ final class PhabricatorSearchController
|
|||
|
||||
// Add the user's query, then save this as a new saved query and send
|
||||
// the user to the results page.
|
||||
$saved->setParameter('query', $request->getStr('query'));
|
||||
$saved->setParameter('query', $query);
|
||||
|
||||
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
|
||||
try {
|
||||
|
|
|
@ -2,7 +2,36 @@
|
|||
|
||||
final class PhabricatorDatasourceEngine extends Phobject {
|
||||
|
||||
private $viewer;
|
||||
|
||||
public function setViewer(PhabricatorUser $viewer) {
|
||||
$this->viewer = $viewer;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getViewer() {
|
||||
return $this->viewer;
|
||||
}
|
||||
|
||||
public function getAllQuickSearchDatasources() {
|
||||
return PhabricatorDatasourceEngineExtension::getAllQuickSearchDatasources();
|
||||
}
|
||||
|
||||
public function newJumpURI($query) {
|
||||
$viewer = $this->getViewer();
|
||||
$extensions = PhabricatorDatasourceEngineExtension::getAllExtensions();
|
||||
|
||||
foreach ($extensions as $extension) {
|
||||
$jump_uri = id(clone $extension)
|
||||
->setViewer($viewer)
|
||||
->newJumpURI($query);
|
||||
|
||||
if ($jump_uri !== null) {
|
||||
return $jump_uri;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,135 +0,0 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorJumpNavHandler extends Phobject {
|
||||
|
||||
public static function getJumpResponse(PhabricatorUser $viewer, $jump) {
|
||||
$jump = trim($jump);
|
||||
|
||||
$patterns = array(
|
||||
'/^a$/i' => 'uri:/diffusion/commit/',
|
||||
'/^f$/i' => 'uri:/feed/',
|
||||
'/^d$/i' => 'uri:/differential/',
|
||||
'/^r$/i' => 'uri:/diffusion/',
|
||||
'/^t$/i' => 'uri:/maniphest/',
|
||||
'/^p$/i' => 'uri:/project/',
|
||||
'/^u$/i' => 'uri:/people/',
|
||||
'/^p\s+(.+)$/i' => 'project',
|
||||
'/^u\s+(\S+)$/i' => 'user',
|
||||
'/^(?:s)\s+(\S+)/i' => 'find-symbol',
|
||||
'/^r\s+(.+)$/i' => 'find-repository',
|
||||
);
|
||||
|
||||
foreach ($patterns as $pattern => $effect) {
|
||||
$matches = null;
|
||||
if (preg_match($pattern, $jump, $matches)) {
|
||||
if (!strncmp($effect, 'uri:', 4)) {
|
||||
return id(new AphrontRedirectResponse())
|
||||
->setURI(substr($effect, 4));
|
||||
} else {
|
||||
switch ($effect) {
|
||||
case 'user':
|
||||
return id(new AphrontRedirectResponse())
|
||||
->setURI('/p/'.$matches[1].'/');
|
||||
case 'project':
|
||||
$project = self::findCloselyNamedProject($matches[1]);
|
||||
if ($project) {
|
||||
return id(new AphrontRedirectResponse())
|
||||
->setURI('/project/view/'.$project->getID().'/');
|
||||
} else {
|
||||
$jump = $matches[1];
|
||||
}
|
||||
break;
|
||||
case 'find-symbol':
|
||||
$context = '';
|
||||
$symbol = $matches[1];
|
||||
$parts = array();
|
||||
if (preg_match('/(.*)(?:\\.|::|->)(.*)/', $symbol, $parts)) {
|
||||
$context = '&context='.phutil_escape_uri($parts[1]);
|
||||
$symbol = $parts[2];
|
||||
}
|
||||
return id(new AphrontRedirectResponse())
|
||||
->setURI("/diffusion/symbol/$symbol/?jump=true$context");
|
||||
case 'find-repository':
|
||||
$raw_query = $matches[1];
|
||||
|
||||
$engine = id(new PhabricatorRepository())
|
||||
->newFerretEngine();
|
||||
|
||||
$compiler = id(new PhutilSearchQueryCompiler())
|
||||
->setEnableFunctions(true);
|
||||
|
||||
$raw_tokens = $compiler->newTokens($raw_query);
|
||||
|
||||
$fulltext_tokens = array();
|
||||
foreach ($raw_tokens as $raw_token) {
|
||||
$fulltext_token = id(new PhabricatorFulltextToken())
|
||||
->setToken($raw_token);
|
||||
$fulltext_tokens[] = $fulltext_token;
|
||||
}
|
||||
|
||||
$repositories = id(new PhabricatorRepositoryQuery())
|
||||
->setViewer($viewer)
|
||||
->withFerretConstraint($engine, $fulltext_tokens)
|
||||
->execute();
|
||||
if (count($repositories) == 1) {
|
||||
// Just one match, jump to repository.
|
||||
$uri = head($repositories)->getURI();
|
||||
} else {
|
||||
// More than one match, jump to search.
|
||||
$uri = urisprintf(
|
||||
'/diffusion/?order=name&query=%s',
|
||||
$raw_query);
|
||||
}
|
||||
return id(new AphrontRedirectResponse())->setURI($uri);
|
||||
default:
|
||||
throw new Exception(pht("Unknown jump effect '%s'!", $effect));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If none of the patterns matched, look for an object by name.
|
||||
$objects = id(new PhabricatorObjectQuery())
|
||||
->setViewer($viewer)
|
||||
->withNames(array($jump))
|
||||
->execute();
|
||||
|
||||
if (count($objects) == 1) {
|
||||
$handle = id(new PhabricatorHandleQuery())
|
||||
->setViewer($viewer)
|
||||
->withPHIDs(mpull($objects, 'getPHID'))
|
||||
->executeOne();
|
||||
|
||||
return id(new AphrontRedirectResponse())->setURI($handle->getURI());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static function findCloselyNamedProject($name) {
|
||||
$project = id(new PhabricatorProject())->loadOneWhere(
|
||||
'name = %s',
|
||||
$name);
|
||||
if ($project) {
|
||||
return $project;
|
||||
} else { // no exact match, try a fuzzy match
|
||||
$projects = id(new PhabricatorProject())->loadAllWhere(
|
||||
'name LIKE %~',
|
||||
$name);
|
||||
if ($projects) {
|
||||
$min_name_length = PHP_INT_MAX;
|
||||
$best_project = null;
|
||||
foreach ($projects as $project) {
|
||||
$name_length = strlen($project->getName());
|
||||
if ($name_length <= $min_name_length) {
|
||||
$min_name_length = $name_length;
|
||||
$best_project = $project;
|
||||
}
|
||||
}
|
||||
return $best_project;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,12 +2,33 @@
|
|||
|
||||
abstract class PhabricatorDatasourceEngineExtension extends Phobject {
|
||||
|
||||
abstract public function newQuickSearchDatasources();
|
||||
private $viewer;
|
||||
|
||||
final public static function getAllQuickSearchDatasources() {
|
||||
$extensions = id(new PhutilClassMapQuery())
|
||||
final public function setViewer(PhabricatorUser $viewer) {
|
||||
$this->viewer = $viewer;
|
||||
return $this;
|
||||
}
|
||||
|
||||
final public function getViewer() {
|
||||
return $this->viewer;
|
||||
}
|
||||
|
||||
public function newQuickSearchDatasources() {
|
||||
return array();
|
||||
}
|
||||
|
||||
public function newJumpURI($query) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final public static function getAllExtensions() {
|
||||
return id(new PhutilClassMapQuery())
|
||||
->setAncestorClass(__CLASS__)
|
||||
->execute();
|
||||
}
|
||||
|
||||
final public static function getAllQuickSearchDatasources() {
|
||||
$extensions = self::getAllExtensions();
|
||||
|
||||
$datasources = array();
|
||||
foreach ($extensions as $extension) {
|
||||
|
|
|
@ -8,4 +8,46 @@ final class PhabricatorMonogramDatasourceEngineExtension
|
|||
new PhabricatorTypeaheadMonogramDatasource(),
|
||||
);
|
||||
}
|
||||
|
||||
public function newJumpURI($query) {
|
||||
$viewer = $this->getViewer();
|
||||
|
||||
// These first few rules are sort of random but don't fit anywhere else
|
||||
// today and don't feel worth adding separate extensions for.
|
||||
|
||||
// Send "f" to Feed.
|
||||
if (preg_match('/^f\z/i', $query)) {
|
||||
return '/feed/';
|
||||
}
|
||||
|
||||
// Send "d" to Differential.
|
||||
if (preg_match('/^d\z/i', $query)) {
|
||||
return '/differential/';
|
||||
}
|
||||
|
||||
// Send "t" to Maniphest.
|
||||
if (preg_match('/^t\z/i', $query)) {
|
||||
return '/maniphest/';
|
||||
}
|
||||
|
||||
// Otherwise, if the user entered an object name, jump to that object.
|
||||
$objects = id(new PhabricatorObjectQuery())
|
||||
->setViewer($viewer)
|
||||
->withNames(array($query))
|
||||
->execute();
|
||||
if (count($objects) == 1) {
|
||||
$object = head($objects);
|
||||
$object_phid = $object->getPHID();
|
||||
|
||||
$handles = $viewer->loadHandles(array($object_phid));
|
||||
$handle = $handles[$object_phid];
|
||||
|
||||
if ($handle->isComplete()) {
|
||||
return $handle->getURI();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue