Page MenuHomestyx hydra

No OneTemporary

diff --git a/src/applications/search/query/PhabricatorFulltextToken.php b/src/applications/search/query/PhabricatorFulltextToken.php
index 4edeb098a9..8dc2cee3ca 100644
--- a/src/applications/search/query/PhabricatorFulltextToken.php
+++ b/src/applications/search/query/PhabricatorFulltextToken.php
@@ -1,88 +1,92 @@
<?php
final class PhabricatorFulltextToken extends Phobject {
private $token;
private $isShort;
private $isStopword;
public function setToken(PhutilSearchQueryToken $token) {
$this->token = $token;
return $this;
}
public function getToken() {
return $this->token;
}
public function isQueryable() {
return !$this->getIsShort() && !$this->getIsStopword();
}
public function setIsShort($is_short) {
$this->isShort = $is_short;
return $this;
}
public function getIsShort() {
return $this->isShort;
}
public function setIsStopword($is_stopword) {
$this->isStopword = $is_stopword;
return $this;
}
public function getIsStopword() {
return $this->isStopword;
}
public function newTag() {
$token = $this->getToken();
$tip = null;
$icon = null;
if ($this->getIsShort()) {
$shade = PHUITagView::COLOR_GREY;
$tip = pht('Ignored Short Word');
} else if ($this->getIsStopword()) {
$shade = PHUITagView::COLOR_GREY;
$tip = pht('Ignored Common Word');
} else {
$operator = $token->getOperator();
switch ($operator) {
case PhutilSearchQueryCompiler::OPERATOR_NOT:
$shade = PHUITagView::COLOR_RED;
$icon = 'fa-minus';
break;
+ case PhutilSearchQueryCompiler::OPERATOR_SUBSTRING:
+ $tip = pht('Substring Search');
+ $shade = PHUITagView::COLOR_VIOLET;
+ break;
default:
$shade = PHUITagView::COLOR_BLUE;
break;
}
}
$tag = id(new PHUITagView())
->setType(PHUITagView::TYPE_SHADE)
->setColor($shade)
->setName($token->getValue());
if ($tip !== null) {
Javelin::initBehavior('phabricator-tooltips');
$tag
->addSigil('has-tooltip')
->setMetadata(
array(
'tip' => $tip,
));
}
if ($icon !== null) {
$tag->setIcon($icon);
}
return $tag;
}
}
diff --git a/src/applications/search/query/PhabricatorSearchApplicationSearchEngine.php b/src/applications/search/query/PhabricatorSearchApplicationSearchEngine.php
index d944d61964..3fcf0a8f7a 100644
--- a/src/applications/search/query/PhabricatorSearchApplicationSearchEngine.php
+++ b/src/applications/search/query/PhabricatorSearchApplicationSearchEngine.php
@@ -1,313 +1,313 @@
<?php
final class PhabricatorSearchApplicationSearchEngine
extends PhabricatorApplicationSearchEngine {
private $resultSet;
public function getResultTypeDescription() {
return pht('Fulltext Search Results');
}
public function getApplicationClassName() {
return 'PhabricatorSearchApplication';
}
public function buildSavedQueryFromRequest(AphrontRequest $request) {
$saved = new PhabricatorSavedQuery();
$saved->setParameter('query', $request->getStr('query'));
$saved->setParameter(
'statuses',
$this->readListFromRequest($request, 'statuses'));
$saved->setParameter(
'types',
$this->readListFromRequest($request, 'types'));
$saved->setParameter(
'authorPHIDs',
$this->readUsersFromRequest($request, 'authorPHIDs'));
$saved->setParameter(
'ownerPHIDs',
$this->readUsersFromRequest($request, 'ownerPHIDs'));
$saved->setParameter(
'subscriberPHIDs',
$this->readSubscribersFromRequest($request, 'subscriberPHIDs'));
$saved->setParameter(
'projectPHIDs',
$this->readPHIDsFromRequest($request, 'projectPHIDs'));
return $saved;
}
public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) {
$query = new PhabricatorSearchDocumentQuery();
// Convert the saved query into a resolved form (without typeahead
// functions) which the fulltext search engines can execute.
$config = clone $saved;
$viewer = $this->requireViewer();
$datasource = id(new PhabricatorPeopleOwnerDatasource())
->setViewer($viewer);
$owner_phids = $this->readOwnerPHIDs($config);
$owner_phids = $datasource->evaluateTokens($owner_phids);
foreach ($owner_phids as $key => $phid) {
if ($phid == PhabricatorPeopleNoOwnerDatasource::FUNCTION_TOKEN) {
$config->setParameter('withUnowned', true);
unset($owner_phids[$key]);
}
if ($phid == PhabricatorPeopleAnyOwnerDatasource::FUNCTION_TOKEN) {
$config->setParameter('withAnyOwner', true);
unset($owner_phids[$key]);
}
}
$config->setParameter('ownerPHIDs', $owner_phids);
$datasource = id(new PhabricatorPeopleUserFunctionDatasource())
->setViewer($viewer);
$author_phids = $config->getParameter('authorPHIDs', array());
$author_phids = $datasource->evaluateTokens($author_phids);
$config->setParameter('authorPHIDs', $author_phids);
$datasource = id(new PhabricatorMetaMTAMailableFunctionDatasource())
->setViewer($viewer);
$subscriber_phids = $config->getParameter('subscriberPHIDs', array());
$subscriber_phids = $datasource->evaluateTokens($subscriber_phids);
$config->setParameter('subscriberPHIDs', $subscriber_phids);
$query->withSavedQuery($config);
return $query;
}
public function buildSearchForm(
AphrontFormView $form,
PhabricatorSavedQuery $saved) {
$options = array();
$author_value = null;
$owner_value = null;
$subscribers_value = null;
$project_value = null;
$author_phids = $saved->getParameter('authorPHIDs', array());
$owner_phids = $this->readOwnerPHIDs($saved);
$subscriber_phids = $saved->getParameter('subscriberPHIDs', array());
$project_phids = $saved->getParameter('projectPHIDs', array());
$status_values = $saved->getParameter('statuses', array());
$status_values = array_fuse($status_values);
$statuses = array(
PhabricatorSearchRelationship::RELATIONSHIP_OPEN => pht('Open'),
PhabricatorSearchRelationship::RELATIONSHIP_CLOSED => pht('Closed'),
);
$status_control = id(new AphrontFormCheckboxControl())
->setLabel(pht('Document Status'));
foreach ($statuses as $status => $name) {
$status_control->addCheckbox(
'statuses[]',
$status,
$name,
isset($status_values[$status]));
}
$type_values = $saved->getParameter('types', array());
$type_values = array_fuse($type_values);
$types_control = id(new AphrontFormTokenizerControl())
->setLabel(pht('Document Types'))
->setName('types')
->setDatasource(new PhabricatorSearchDocumentTypeDatasource())
->setValue($type_values);
$form
->appendChild(
phutil_tag(
'input',
array(
'type' => 'hidden',
'name' => 'jump',
'value' => 'no',
)))
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Query'))
->setName('query')
->setValue($saved->getParameter('query')))
->appendChild($status_control)
->appendControl($types_control)
->appendControl(
id(new AphrontFormTokenizerControl())
->setName('authorPHIDs')
->setLabel(pht('Authors'))
->setDatasource(new PhabricatorPeopleUserFunctionDatasource())
->setValue($author_phids))
->appendControl(
id(new AphrontFormTokenizerControl())
->setName('ownerPHIDs')
->setLabel(pht('Owners'))
->setDatasource(new PhabricatorPeopleOwnerDatasource())
->setValue($owner_phids))
->appendControl(
id(new AphrontFormTokenizerControl())
->setName('subscriberPHIDs')
->setLabel(pht('Subscribers'))
->setDatasource(new PhabricatorMetaMTAMailableFunctionDatasource())
->setValue($subscriber_phids))
->appendControl(
id(new AphrontFormTokenizerControl())
->setName('projectPHIDs')
->setLabel(pht('Tags'))
->setDatasource(new PhabricatorProjectDatasource())
->setValue($project_phids));
}
protected function getURI($path) {
return '/search/'.$path;
}
protected function getBuiltinQueryNames() {
return array(
'all' => pht('All Documents'),
'open' => pht('Open Documents'),
'open-tasks' => pht('Open Tasks'),
);
}
public function buildSavedQueryFromBuiltin($query_key) {
$query = $this->newSavedQuery();
$query->setQueryKey($query_key);
switch ($query_key) {
case 'all':
return $query;
case 'open':
return $query->setParameter('statuses', array('open'));
case 'open-tasks':
return $query
->setParameter('statuses', array('open'))
->setParameter('types', array(ManiphestTaskPHIDType::TYPECONST));
}
return parent::buildSavedQueryFromBuiltin($query_key);
}
public static function getIndexableDocumentTypes(
PhabricatorUser $viewer = null) {
// TODO: This is inelegant and not very efficient, but gets us reasonable
// results. It would be nice to do this more elegantly.
$objects = id(new PhutilClassMapQuery())
->setAncestorClass('PhabricatorFulltextInterface')
->execute();
$type_map = array();
foreach ($objects as $object) {
$phid_type = phid_get_type($object->generatePHID());
$type_map[$phid_type] = $object;
}
if ($viewer) {
$types = PhabricatorPHIDType::getAllInstalledTypes($viewer);
} else {
$types = PhabricatorPHIDType::getAllTypes();
}
$results = array();
foreach ($types as $type) {
$typeconst = $type->getTypeConstant();
$object = idx($type_map, $typeconst);
if ($object) {
$results[$typeconst] = $type->getTypeName();
}
}
asort($results);
return $results;
}
public function shouldUseOffsetPaging() {
return true;
}
protected function renderResultList(
array $results,
PhabricatorSavedQuery $query,
array $handles) {
$result_set = $this->resultSet;
$fulltext_tokens = $result_set->getFulltextTokens();
$viewer = $this->requireViewer();
$list = new PHUIObjectItemListView();
$list->setNoDataString(pht('No results found.'));
if ($results) {
$objects = id(new PhabricatorObjectQuery())
->setViewer($viewer)
->withPHIDs(mpull($results, 'getPHID'))
->execute();
foreach ($results as $phid => $handle) {
$view = id(new PhabricatorSearchResultView())
->setHandle($handle)
- ->setQuery($query)
+ ->setTokens($fulltext_tokens)
->setObject(idx($objects, $phid))
->render();
$list->addItem($view);
}
}
$fulltext_view = null;
if ($fulltext_tokens) {
require_celerity_resource('phabricator-search-results-css');
$fulltext_view = array();
foreach ($fulltext_tokens as $token) {
$fulltext_view[] = $token->newTag();
}
$fulltext_view = phutil_tag(
'div',
array(
'class' => 'phui-fulltext-tokens',
),
array(
pht('Searched For:'),
' ',
$fulltext_view,
));
}
$result = new PhabricatorApplicationSearchResultView();
$result->setContent($fulltext_view);
$result->setObjectList($list);
return $result;
}
private function readOwnerPHIDs(PhabricatorSavedQuery $saved) {
$owner_phids = $saved->getParameter('ownerPHIDs', array());
// This was an old checkbox from before typeahead functions.
if ($saved->getParameter('withUnowned')) {
$owner_phids[] = PhabricatorPeopleNoOwnerDatasource::FUNCTION_TOKEN;
}
return $owner_phids;
}
protected function didExecuteQuery(PhabricatorPolicyAwareQuery $query) {
$this->resultSet = $query->getFulltextResultSet();
}
}
diff --git a/src/applications/search/view/PhabricatorSearchResultView.php b/src/applications/search/view/PhabricatorSearchResultView.php
index 63219b3252..8819f41869 100644
--- a/src/applications/search/view/PhabricatorSearchResultView.php
+++ b/src/applications/search/view/PhabricatorSearchResultView.php
@@ -1,143 +1,185 @@
<?php
final class PhabricatorSearchResultView extends AphrontView {
private $handle;
- private $query;
private $object;
+ private $tokens;
public function setHandle(PhabricatorObjectHandle $handle) {
$this->handle = $handle;
return $this;
}
- public function setQuery(PhabricatorSavedQuery $query) {
- $this->query = $query;
+ public function setTokens(array $tokens) {
+ assert_instances_of($tokens, 'PhabricatorFulltextToken');
+ $this->tokens = $tokens;
return $this;
}
public function setObject($object) {
$this->object = $object;
return $this;
}
public function render() {
$handle = $this->handle;
if (!$handle->isComplete()) {
return;
}
require_celerity_resource('phabricator-search-results-css');
$type_name = nonempty($handle->getTypeName(), pht('Document'));
$raw_title = $handle->getFullName();
$title = $this->emboldenQuery($raw_title);
$item = id(new PHUIObjectItemView())
->setHeader($title)
->setTitleText($raw_title)
->setHref($handle->getURI())
->setImageURI($handle->getImageURI())
->addAttribute($type_name);
if ($handle->getStatus() == PhabricatorObjectHandle::STATUS_CLOSED) {
$item->setDisabled(true);
$item->addAttribute(pht('Closed'));
}
return $item;
}
/**
* Find the words which are part of the query string, and bold them in a
* result string. This makes it easier for users to see why a result
* matched their query.
*/
private function emboldenQuery($str) {
- $query = $this->query->getParameter('query');
+ $tokens = $this->tokens;
- if (!strlen($query) || !strlen($str)) {
+ if (!$tokens) {
return $str;
}
- // This algorithm is safe but not especially fast, so don't bother if
- // we're dealing with a lot of data. This mostly prevents silly/malicious
- // queries from doing anything bad.
- if (strlen($query) + strlen($str) > 2048) {
+ if (count($tokens) > 16) {
return $str;
}
- // Keep track of which characters we're going to make bold. This is
- // byte oriented, but we'll make sure we don't put a bold in the middle
- // of a character later.
- $bold = array_fill(0, strlen($str), false);
+ if (!strlen($str)) {
+ return $str;
+ }
- // Split the query into words.
- $parts = preg_split('/ +/', $query);
+ if (strlen($str) > 2048) {
+ return $str;
+ }
- // Find all occurrences of each word, and mark them to be emboldened.
- foreach ($parts as $part) {
- $part = trim($part);
- $part = trim($part, '"+');
- if (!strlen($part)) {
- continue;
+ $patterns = array();
+ foreach ($tokens as $token) {
+ $raw_token = $token->getToken();
+ $operator = $raw_token->getOperator();
+
+ $value = $raw_token->getValue();
+
+ switch ($operator) {
+ case PhutilSearchQueryCompiler::OPERATOR_SUBSTRING:
+ $patterns[] = '(('.preg_quote($value).'))ui';
+ break;
+ case PhutilSearchQueryCompiler::OPERATOR_AND:
+ $patterns[] = '((?<=\W|^)('.preg_quote($value).')(?=\W|\z))ui';
+ break;
+ default:
+ // Don't highlight anything else, particularly "NOT".
+ break;
}
+ }
+ // Find all matches for all query terms in the document title, then reduce
+ // them to a map from offsets to highlighted sequence lengths. If two terms
+ // match at the same position, we choose the longer one.
+ $all_matches = array();
+ foreach ($patterns as $pattern) {
$matches = null;
- $has_matches = preg_match_all(
- '/(?:^|\b)('.preg_quote($part, '/').')/i',
+ $ok = preg_match_all(
+ $pattern,
$str,
$matches,
PREG_OFFSET_CAPTURE);
-
- if (!$has_matches) {
+ if (!$ok) {
continue;
}
- // Flag the matching part of the range for boldening.
foreach ($matches[1] as $match) {
- $offset = $match[1];
- for ($ii = 0; $ii < strlen($match[0]); $ii++) {
- $bold[$offset + $ii] = true;
+ $match_text = $match[0];
+ $match_offset = $match[1];
+
+ if (!isset($all_matches[$match_offset])) {
+ $all_matches[$match_offset] = 0;
}
+
+ $all_matches[$match_offset] = max(
+ $all_matches[$match_offset],
+ strlen($match_text));
}
}
- // Split the string into ranges, applying bold styling as required.
- $out = array();
- $buf = '';
- $pos = 0;
- $is_bold = false;
-
- // Make sure this is UTF8 because phutil_utf8v() will explode if it isn't.
- $str = phutil_utf8ize($str);
- foreach (phutil_utf8v($str) as $chr) {
- if ($bold[$pos] != $is_bold) {
- if (strlen($buf)) {
- if ($is_bold) {
- $out[] = phutil_tag('strong', array(), $buf);
- } else {
- $out[] = $buf;
- }
- $buf = '';
+ // Go through the string one display glyph at a time. If a glyph starts
+ // on a highlighted byte position, turn on highlighting for the nubmer
+ // of matching bytes. If a query searches for "e" and the document contains
+ // an "e" followed by a bunch of combining marks, this will correctly
+ // highlight the entire glyph.
+ $parts = array();
+ $highlight = 0;
+ $offset = 0;
+ foreach (phutil_utf8v_combined($str) as $character) {
+ $length = strlen($character);
+
+ if (isset($all_matches[$offset])) {
+ $highlight = $all_matches[$offset];
+ }
+
+ if ($highlight > 0) {
+ $is_highlighted = true;
+ $highlight -= $length;
+ } else {
+ $is_highlighted = false;
+ }
+
+ $parts[] = array(
+ 'text' => $character,
+ 'highlighted' => $is_highlighted,
+ );
+
+ $offset += $length;
+ }
+
+ // Combine all the sequences together so we aren't emitting a tag around
+ // every individual character.
+ $last = null;
+ foreach ($parts as $key => $part) {
+ if ($last !== null) {
+ if ($part['highlighted'] == $parts[$last]['highlighted']) {
+ $parts[$last]['text'] .= $part['text'];
+ unset($parts[$key]);
+ continue;
}
- $is_bold = !$is_bold;
}
- $buf .= $chr;
- $pos += strlen($chr);
+
+ $last = $key;
}
- if (strlen($buf)) {
- if ($is_bold) {
- $out[] = phutil_tag('strong', array(), $buf);
+ // Finally, add tags.
+ $result = array();
+ foreach ($parts as $part) {
+ if ($part['highlighted']) {
+ $result[] = phutil_tag('strong', array(), $part['text']);
} else {
- $out[] = $buf;
+ $result[] = $part['text'];
}
}
- return $out;
+ return $result;
}
}

File Metadata

Mime Type
text/x-diff
Expires
Thu, May 1, 5:01 PM (1 d, 16 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
109051
Default Alt Text
(19 KB)

Event Timeline