diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 95678635b3..e88762b70b 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1362,6 +1362,8 @@ phutil_register_library_map(array( 'PHUIDiffInlineCommentView' => 'infrastructure/diff/view/PHUIDiffInlineCommentView.php', 'PHUIDiffOneUpInlineCommentRowScaffold' => 'infrastructure/diff/view/PHUIDiffOneUpInlineCommentRowScaffold.php', 'PHUIDiffRevealIconView' => 'infrastructure/diff/view/PHUIDiffRevealIconView.php', + 'PHUIDiffTableOfContentsItemView' => 'infrastructure/diff/view/PHUIDiffTableOfContentsItemView.php', + 'PHUIDiffTableOfContentsListView' => 'infrastructure/diff/view/PHUIDiffTableOfContentsListView.php', 'PHUIDiffTwoUpInlineCommentRowScaffold' => 'infrastructure/diff/view/PHUIDiffTwoUpInlineCommentRowScaffold.php', 'PHUIDocumentExample' => 'applications/uiexample/examples/PHUIDocumentExample.php', 'PHUIDocumentView' => 'view/phui/PHUIDocumentView.php', @@ -5157,6 +5159,8 @@ phutil_register_library_map(array( 'PHUIDiffInlineCommentView' => 'AphrontView', 'PHUIDiffOneUpInlineCommentRowScaffold' => 'PHUIDiffInlineCommentRowScaffold', 'PHUIDiffRevealIconView' => 'AphrontView', + 'PHUIDiffTableOfContentsItemView' => 'AphrontView', + 'PHUIDiffTableOfContentsListView' => 'AphrontView', 'PHUIDiffTwoUpInlineCommentRowScaffold' => 'PHUIDiffInlineCommentRowScaffold', 'PHUIDocumentExample' => 'PhabricatorUIExample', 'PHUIDocumentView' => 'AphrontTagView', diff --git a/src/applications/differential/controller/DifferentialRevisionViewController.php b/src/applications/differential/controller/DifferentialRevisionViewController.php index aa8daa76d6..c853cc278c 100644 --- a/src/applications/differential/controller/DifferentialRevisionViewController.php +++ b/src/applications/differential/controller/DifferentialRevisionViewController.php @@ -349,16 +349,10 @@ final class DifferentialRevisionViewController extends DifferentialController { $other_view = $this->renderOtherRevisions($other_revisions); } - $toc_view = new DifferentialDiffTableOfContentsView(); - $toc_view->setChangesets($changesets); - $toc_view->setVisibleChangesets($visible_changesets); - $toc_view->setRenderingReferences($rendering_references); - $toc_view->setCoverageMap($target->loadCoverageMap($user)); - if ($repository) { - $toc_view->setRepository($repository); - } - $toc_view->setDiff($target); - $toc_view->setUser($user); + $toc_view = $this->buildTableOfContents( + $changesets, + $visible_changesets, + $target->loadCoverageMap($user)); $comment_form = null; if (!$viewer_is_anonymous) { @@ -1042,5 +1036,34 @@ final class DifferentialRevisionViewController extends DifferentialController { return $view; } + private function buildTableOfContents( + array $changesets, + array $visible_changesets, + array $coverage) { + $viewer = $this->getViewer(); + + $toc_view = id(new PHUIDiffTableOfContentsListView()) + ->setUser($viewer); + + foreach ($changesets as $changeset_id => $changeset) { + $is_visible = isset($visible_changesets[$changeset_id]); + $anchor = $changeset->getAnchorName(); + + $filename = $changeset->getFilename(); + $coverage_id = 'differential-mcoverage-'.md5($filename); + + $item = id(new PHUIDiffTableOfContentsItemView()) + ->setChangeset($changeset) + ->setIsVisible($is_visible) + ->setAnchor($anchor) + ->setCoverage(idx($coverage, $filename)) + ->setCoverageID($coverage_id); + + $toc_view->addItem($item); + } + + return $toc_view; + } + } diff --git a/src/infrastructure/diff/view/PHUIDiffTableOfContentsItemView.php b/src/infrastructure/diff/view/PHUIDiffTableOfContentsItemView.php new file mode 100644 index 0000000000..638a9c21eb --- /dev/null +++ b/src/infrastructure/diff/view/PHUIDiffTableOfContentsItemView.php @@ -0,0 +1,295 @@ +changeset = $changeset; + return $this; + } + + public function getChangeset() { + return $this->changeset; + } + + public function setIsVisible($is_visible) { + $this->isVisible = $is_visible; + return $this; + } + + public function getIsVisible() { + return $this->isVisible; + } + + public function setAnchor($anchor) { + $this->anchor = $anchor; + return $this; + } + + public function getAnchor() { + return $this->anchor; + } + + public function setCoverage($coverage) { + $this->coverage = $coverage; + return $this; + } + + public function getCoverage() { + return $this->coverage; + } + + public function setCoverageID($coverage_id) { + $this->coverageID = $coverage_id; + return $this; + } + + public function getCoverageID() { + return $this->coverageID; + } + + public function render() { + $changeset = $this->getChangeset(); + + $cells = array(); + + $cells[] = $this->renderPathChangeCharacter(); + $cells[] = $this->renderPropertyChangeCharacter(); + $cells[] = $this->renderPropertyChangeDescription(); + + $link = $this->renderChangesetLink(); + $lines = $this->renderChangesetLines(); + $meta = $this->renderChangesetMetadata(); + + $cells[] = array( + $link, + $lines, + $meta, + ); + + $cells[] = $this->renderCoverage(); + $cells[] = $this->renderModifiedCoverage(); + + return $cells; + } + + private function renderPathChangeCharacter() { + $changeset = $this->getChangeset(); + $type = $changeset->getChangeType(); + + $color = DifferentialChangeType::getSummaryColorForChangeType($type); + $char = DifferentialChangeType::getSummaryCharacterForChangeType($type); + $title = DifferentialChangeType::getFullNameForChangeType($type); + + return javelin_tag( + 'span', + array( + 'sigil' => 'has-tip', + 'meta' => array( + 'tip' => $title, + 'align' => 'E', + ), + 'class' => 'phui-text-'.$color, + ), + $char); + } + + private function renderPropertyChangeCharacter() { + $changeset = $this->getChangeset(); + + $old = $changeset->getOldProperties(); + $new = $changeset->getNewProperties(); + + if ($old === $new) { + return null; + } + + return javelin_tag( + 'span', + array( + 'sigil' => 'has-tip', + 'meta' => array( + 'tip' => pht('Properties Modified'), + 'align' => 'E', + ), + ), + 'M'); + } + + private function renderPropertyChangeDescription() { + $changeset = $this->getChangeset(); + + $file_type = $changeset->getFileType(); + + $desc = DifferentialChangeType::getShortNameForFileType($file_type); + if ($desc === null) { + return null; + } + + return pht('(%s)', $desc); + } + + private function renderChangesetLink() { + $anchor = $this->getAnchor(); + + $changeset = $this->getChangeset(); + $name = $changeset->getDisplayFilename(); + + $change_type = $changeset->getChangeType(); + if (DifferentialChangeType::isOldLocationChangeType($change_type)) { + $away = $changeset->getAwayPaths(); + if (count($away) == 1) { + if ($change_type == DifferentialChangeType::TYPE_MOVE_AWAY) { + $right_arrow = "\xE2\x86\x92"; + $name = $this->renderRename($name, head($away), $right_arrow); + } + } + } else if ($change_type == DifferentialChangeType::TYPE_MOVE_HERE) { + $left_arrow = "\xE2\x86\x90"; + $name = $this->renderRename($name, $changeset->getOldFile(), $left_arrow); + } + + return javelin_tag( + 'a', + array( + 'href' => '#'.$anchor, + 'sigil' => 'differential-load', + 'meta' => array( + 'id' => 'diff-'.$anchor, + ), + ), + $name); + } + + private function renderChangesetLines() { + $changeset = $this->getChangeset(); + + $line_count = $changeset->getAffectedLineCount(); + if (!$line_count) { + return null; + } + + return ' '.pht('(%d line(s))', $line_count); + } + + private function renderCoverage() { + $not_applicable = '-'; + + $coverage = $this->getCoverage(); + if (!strlen($coverage)) { + return $not_applicable; + } + + $covered = substr_count($coverage, 'C'); + $not_covered = substr_count($coverage, 'U'); + + if (!$not_covered && !$covered) { + return $not_applicable; + } + + return sprintf('%d%%', 100 * ($covered / ($covered + $not_covered))); + } + + private function renderModifiedCoverage() { + $not_applicable = '-'; + + $coverage = $this->getCoverage(); + if (!strlen($coverage)) { + return $not_applicable; + } + + if ($this->getIsVisible()) { + $label = pht('Loading...'); + } else { + $label = pht('?'); + } + + return phutil_tag( + 'div', + array( + 'id' => $this->getCoverageID(), + 'class' => 'differential-mcoverage-loading', + ), + $label); + } + + private function renderChangesetMetadata() { + $changeset = $this->getChangeset(); + $type = $changeset->getChangeType(); + + $meta = array(); + if (DifferentialChangeType::isOldLocationChangeType($type)) { + $away = $changeset->getAwayPaths(); + if (count($away) > 1) { + if ($type == DifferentialChangeType::TYPE_MULTICOPY) { + $meta[] = pht('Deleted after being copied to multiple locations:'); + } else { + $meta[] = pht('Copied to multiple locations:'); + } + foreach ($away as $path) { + $meta[] = $path; + } + } else { + if ($type == DifferentialChangeType::TYPE_MOVE_AWAY) { + // This case is handled when we render the path. + } else { + $meta[] = pht('Copied to %s', head($away)); + } + } + } else if ($type == DifferentialChangeType::TYPE_COPY_HERE) { + $meta = pht('Copied from %s', $changeset->getOldFile()); + } + + if (!$meta) { + return null; + } + + $meta = phutil_implode_html(phutil_tag('br'), $meta); + + return phutil_tag( + 'div', + array( + 'class' => 'differential-toc-meta', + ), + $meta); + } + + private function renderRename($self, $other, $arrow) { + $old = explode('/', $self); + $new = explode('/', $other); + + $start = count($old); + foreach ($old as $index => $part) { + if (!isset($new[$index]) || $part != $new[$index]) { + $start = $index; + break; + } + } + + $end = count($old); + foreach (array_reverse($old) as $from_end => $part) { + $index = count($new) - $from_end - 1; + if (!isset($new[$index]) || $part != $new[$index]) { + $end = $from_end; + break; + } + } + + $rename = + '{'. + implode('/', array_slice($old, $start, count($old) - $end - $start)). + ' '.$arrow.' '. + implode('/', array_slice($new, $start, count($new) - $end - $start)). + '}'; + + array_splice($new, $start, count($new) - $end - $start, $rename); + + return implode('/', $new); + } + +} diff --git a/src/infrastructure/diff/view/PHUIDiffTableOfContentsListView.php b/src/infrastructure/diff/view/PHUIDiffTableOfContentsListView.php new file mode 100644 index 0000000000..6ed2e8343e --- /dev/null +++ b/src/infrastructure/diff/view/PHUIDiffTableOfContentsListView.php @@ -0,0 +1,80 @@ +items[] = $item; + return $this; + } + + public function render() { + $this->requireResource('differential-core-view-css'); + $this->requireResource('differential-table-of-contents-css'); + $this->requireResource('phui-text-css'); + + $items = $this->items; + + $rows = array(); + foreach ($items as $item) { + $rows[] = $item->render(); + } + + $reveal_link = javelin_tag( + 'a', + array( + 'sigil' => 'differential-reveal-all', + 'mustcapture' => true, + 'class' => 'button differential-toc-reveal-all', + ), + pht('Show All Context')); + + $buttons = phutil_tag( + 'div', + array( + 'class' => 'differential-toc-buttons grouped', + ), + $reveal_link); + + $table = id(new AphrontTableView($rows)) + ->setHeaders( + array( + '', + '', + '', + pht('Path'), + pht('Coverage (All)'), + pht('Coverage (Touched)'), + )) + ->setColumnClasses( + array( + 'differential-toc-char center', + 'differential-toc-prop center', + 'differential-toc-ftype center', + 'differential-toc-file wide', + 'differential-toc-cov', + 'differential-toc-cov', + )) + ->setDeviceVisibility( + array( + true, + true, + true, + true, + false, + false, + )); + + $anchor = id(new PhabricatorAnchorView()) + ->setAnchorName('toc') + ->setNavigationMarker(true); + + return id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Table of Contents')) + ->setTable($table) + ->appendChild($anchor) + ->appendChild($buttons); + } + +}