1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-25 16:22:43 +01:00

Roughly support inline comment suggestions

Summary:
Ref T13513. This still has quite a few rough edges and some significant performance isssues, but appears to mostly work.

Allow reviewers to "Suggest Edit" on an inline comment and provide replacement text for the highlighted source.

Test Plan: Created, edited, reloaded, and submitted inline comments in various states with and without suggestion text.

Maniphest Tasks: T13513

Differential Revision: https://secure.phabricator.com/D21276
This commit is contained in:
epriestley 2020-05-19 14:50:47 -07:00
parent 00430fdbe1
commit 846562158a
18 changed files with 615 additions and 55 deletions

View file

@ -12,8 +12,8 @@ return array(
'core.pkg.css' => 'ba768cdb', 'core.pkg.css' => 'ba768cdb',
'core.pkg.js' => '845355f4', 'core.pkg.js' => '845355f4',
'dark-console.pkg.js' => '187792c2', 'dark-console.pkg.js' => '187792c2',
'differential.pkg.css' => '42a2334f', 'differential.pkg.css' => 'f924dbcf',
'differential.pkg.js' => 'd0ddfb19', 'differential.pkg.js' => '256a327a',
'diffusion.pkg.css' => '42c75c37', 'diffusion.pkg.css' => '42c75c37',
'diffusion.pkg.js' => 'a98c0bf7', 'diffusion.pkg.js' => 'a98c0bf7',
'maniphest.pkg.css' => '35995d6d', 'maniphest.pkg.css' => '35995d6d',
@ -65,7 +65,7 @@ return array(
'rsrc/css/application/differential/add-comment.css' => '7e5900d9', 'rsrc/css/application/differential/add-comment.css' => '7e5900d9',
'rsrc/css/application/differential/changeset-view.css' => '60c3d405', 'rsrc/css/application/differential/changeset-view.css' => '60c3d405',
'rsrc/css/application/differential/core.css' => '7300a73e', 'rsrc/css/application/differential/core.css' => '7300a73e',
'rsrc/css/application/differential/phui-inline-comment.css' => 'd5749acc', 'rsrc/css/application/differential/phui-inline-comment.css' => '4107254a',
'rsrc/css/application/differential/revision-comment.css' => '7dbc8d1d', 'rsrc/css/application/differential/revision-comment.css' => '7dbc8d1d',
'rsrc/css/application/differential/revision-history.css' => '8aa3eac5', 'rsrc/css/application/differential/revision-history.css' => '8aa3eac5',
'rsrc/css/application/differential/revision-list.css' => '93d2df7d', 'rsrc/css/application/differential/revision-list.css' => '93d2df7d',
@ -381,7 +381,7 @@ return array(
'rsrc/js/application/dashboard/behavior-dashboard-tab-panel.js' => '0116d3e8', 'rsrc/js/application/dashboard/behavior-dashboard-tab-panel.js' => '0116d3e8',
'rsrc/js/application/diff/DiffChangeset.js' => '6e5e03d2', 'rsrc/js/application/diff/DiffChangeset.js' => '6e5e03d2',
'rsrc/js/application/diff/DiffChangesetList.js' => 'b51ba93a', 'rsrc/js/application/diff/DiffChangesetList.js' => 'b51ba93a',
'rsrc/js/application/diff/DiffInline.js' => '6fa445ef', 'rsrc/js/application/diff/DiffInline.js' => '829b88bf',
'rsrc/js/application/diff/DiffPathView.js' => '8207abf9', 'rsrc/js/application/diff/DiffPathView.js' => '8207abf9',
'rsrc/js/application/diff/DiffTreeView.js' => '5d83623b', 'rsrc/js/application/diff/DiffTreeView.js' => '5d83623b',
'rsrc/js/application/differential/behavior-diff-radios.js' => '925fe8cd', 'rsrc/js/application/differential/behavior-diff-radios.js' => '925fe8cd',
@ -776,7 +776,7 @@ return array(
'phabricator-dashboard-css' => '5a205b9d', 'phabricator-dashboard-css' => '5a205b9d',
'phabricator-diff-changeset' => '6e5e03d2', 'phabricator-diff-changeset' => '6e5e03d2',
'phabricator-diff-changeset-list' => 'b51ba93a', 'phabricator-diff-changeset-list' => 'b51ba93a',
'phabricator-diff-inline' => '6fa445ef', 'phabricator-diff-inline' => '829b88bf',
'phabricator-diff-path-view' => '8207abf9', 'phabricator-diff-path-view' => '8207abf9',
'phabricator-diff-tree-view' => '5d83623b', 'phabricator-diff-tree-view' => '5d83623b',
'phabricator-drag-and-drop-file-upload' => '4370900d', 'phabricator-drag-and-drop-file-upload' => '4370900d',
@ -854,7 +854,7 @@ return array(
'phui-icon-view-css' => '4cbc684a', 'phui-icon-view-css' => '4cbc684a',
'phui-image-mask-css' => '62c7f4d2', 'phui-image-mask-css' => '62c7f4d2',
'phui-info-view-css' => 'a10a909b', 'phui-info-view-css' => 'a10a909b',
'phui-inline-comment-view-css' => 'd5749acc', 'phui-inline-comment-view-css' => '4107254a',
'phui-invisible-character-view-css' => 'c694c4a4', 'phui-invisible-character-view-css' => 'c694c4a4',
'phui-left-right-css' => '68513c34', 'phui-left-right-css' => '68513c34',
'phui-lightbox-css' => '4ebf22da', 'phui-lightbox-css' => '4ebf22da',
@ -1561,9 +1561,6 @@ return array(
'phabricator-diff-path-view', 'phabricator-diff-path-view',
'phuix-button-view', 'phuix-button-view',
), ),
'6fa445ef' => array(
'javelin-dom',
),
70245195 => array( 70245195 => array(
'javelin-behavior', 'javelin-behavior',
'javelin-stratcom', 'javelin-stratcom',
@ -1642,6 +1639,9 @@ return array(
'8207abf9' => array( '8207abf9' => array(
'javelin-dom', 'javelin-dom',
), ),
'829b88bf' => array(
'javelin-dom',
),
83754533 => array( 83754533 => array(
'javelin-install', 'javelin-install',
'javelin-util', 'javelin-util',

View file

@ -3161,6 +3161,7 @@ phutil_register_library_map(array(
'PhabricatorDeveloperConfigOptions' => 'applications/config/option/PhabricatorDeveloperConfigOptions.php', 'PhabricatorDeveloperConfigOptions' => 'applications/config/option/PhabricatorDeveloperConfigOptions.php',
'PhabricatorDeveloperPreferencesSettingsPanel' => 'applications/settings/panel/PhabricatorDeveloperPreferencesSettingsPanel.php', 'PhabricatorDeveloperPreferencesSettingsPanel' => 'applications/settings/panel/PhabricatorDeveloperPreferencesSettingsPanel.php',
'PhabricatorDiffInlineCommentContentState' => 'infrastructure/diff/inline/PhabricatorDiffInlineCommentContentState.php', 'PhabricatorDiffInlineCommentContentState' => 'infrastructure/diff/inline/PhabricatorDiffInlineCommentContentState.php',
'PhabricatorDiffInlineCommentContext' => 'infrastructure/diff/inline/PhabricatorDiffInlineCommentContext.php',
'PhabricatorDiffInlineCommentQuery' => 'infrastructure/diff/query/PhabricatorDiffInlineCommentQuery.php', 'PhabricatorDiffInlineCommentQuery' => 'infrastructure/diff/query/PhabricatorDiffInlineCommentQuery.php',
'PhabricatorDiffPreferencesSettingsPanel' => 'applications/settings/panel/PhabricatorDiffPreferencesSettingsPanel.php', 'PhabricatorDiffPreferencesSettingsPanel' => 'applications/settings/panel/PhabricatorDiffPreferencesSettingsPanel.php',
'PhabricatorDiffScopeEngine' => 'infrastructure/diff/PhabricatorDiffScopeEngine.php', 'PhabricatorDiffScopeEngine' => 'infrastructure/diff/PhabricatorDiffScopeEngine.php',
@ -3594,6 +3595,7 @@ phutil_register_library_map(array(
'PhabricatorInlineComment' => 'infrastructure/diff/interface/PhabricatorInlineComment.php', 'PhabricatorInlineComment' => 'infrastructure/diff/interface/PhabricatorInlineComment.php',
'PhabricatorInlineCommentAdjustmentEngine' => 'infrastructure/diff/engine/PhabricatorInlineCommentAdjustmentEngine.php', 'PhabricatorInlineCommentAdjustmentEngine' => 'infrastructure/diff/engine/PhabricatorInlineCommentAdjustmentEngine.php',
'PhabricatorInlineCommentContentState' => 'infrastructure/diff/inline/PhabricatorInlineCommentContentState.php', 'PhabricatorInlineCommentContentState' => 'infrastructure/diff/inline/PhabricatorInlineCommentContentState.php',
'PhabricatorInlineCommentContext' => 'infrastructure/diff/inline/PhabricatorInlineCommentContext.php',
'PhabricatorInlineCommentController' => 'infrastructure/diff/PhabricatorInlineCommentController.php', 'PhabricatorInlineCommentController' => 'infrastructure/diff/PhabricatorInlineCommentController.php',
'PhabricatorInlineCommentInterface' => 'applications/transactions/interface/PhabricatorInlineCommentInterface.php', 'PhabricatorInlineCommentInterface' => 'applications/transactions/interface/PhabricatorInlineCommentInterface.php',
'PhabricatorInlineSummaryView' => 'infrastructure/diff/view/PhabricatorInlineSummaryView.php', 'PhabricatorInlineSummaryView' => 'infrastructure/diff/view/PhabricatorInlineSummaryView.php',
@ -9630,6 +9632,7 @@ phutil_register_library_map(array(
'PhabricatorDeveloperConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorDeveloperConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorDeveloperPreferencesSettingsPanel' => 'PhabricatorEditEngineSettingsPanel', 'PhabricatorDeveloperPreferencesSettingsPanel' => 'PhabricatorEditEngineSettingsPanel',
'PhabricatorDiffInlineCommentContentState' => 'PhabricatorInlineCommentContentState', 'PhabricatorDiffInlineCommentContentState' => 'PhabricatorInlineCommentContentState',
'PhabricatorDiffInlineCommentContext' => 'PhabricatorInlineCommentContext',
'PhabricatorDiffInlineCommentQuery' => 'PhabricatorApplicationTransactionCommentQuery', 'PhabricatorDiffInlineCommentQuery' => 'PhabricatorApplicationTransactionCommentQuery',
'PhabricatorDiffPreferencesSettingsPanel' => 'PhabricatorEditEngineSettingsPanel', 'PhabricatorDiffPreferencesSettingsPanel' => 'PhabricatorEditEngineSettingsPanel',
'PhabricatorDiffScopeEngine' => 'Phobject', 'PhabricatorDiffScopeEngine' => 'Phobject',
@ -10122,6 +10125,7 @@ phutil_register_library_map(array(
), ),
'PhabricatorInlineCommentAdjustmentEngine' => 'Phobject', 'PhabricatorInlineCommentAdjustmentEngine' => 'Phobject',
'PhabricatorInlineCommentContentState' => 'Phobject', 'PhabricatorInlineCommentContentState' => 'Phobject',
'PhabricatorInlineCommentContext' => 'Phobject',
'PhabricatorInlineCommentController' => 'PhabricatorController', 'PhabricatorInlineCommentController' => 'PhabricatorController',
'PhabricatorInlineSummaryView' => 'AphrontView', 'PhabricatorInlineSummaryView' => 'AphrontView',
'PhabricatorInstructionsEditField' => 'PhabricatorEditField', 'PhabricatorInstructionsEditField' => 'PhabricatorEditField',

View file

@ -17,6 +17,7 @@ final class PhabricatorAuditTransactionComment
protected $attributes = array(); protected $attributes = array();
private $replyToComment = self::ATTACHABLE; private $replyToComment = self::ATTACHABLE;
private $inlineContext = self::ATTACHABLE;
public function getApplicationTransactionObject() { public function getApplicationTransactionObject() {
return new PhabricatorAuditTransaction(); return new PhabricatorAuditTransaction();
@ -83,12 +84,18 @@ final class PhabricatorAuditTransactionComment
return $this; return $this;
} }
public function isEmptyInlineComment() {
return !strlen($this->getContent());
}
public function newInlineCommentObject() { public function newInlineCommentObject() {
return PhabricatorAuditInlineComment::newFromModernComment($this); return PhabricatorAuditInlineComment::newFromModernComment($this);
} }
public function getInlineContext() {
return $this->assertAttached($this->inlineContext);
}
public function attachInlineContext(
PhabricatorInlineCommentContext $context = null) {
$this->inlineContext = $context;
return $this;
}
} }

View file

@ -200,6 +200,7 @@ final class DifferentialChangesetViewController extends DifferentialController {
->withPublishableComments(true) ->withPublishableComments(true)
->withPublishedComments(true) ->withPublishedComments(true)
->needHidden(true) ->needHidden(true)
->needInlineContext(true)
->execute(); ->execute();
$inlines = mpull($inlines, 'newInlineCommentObject'); $inlines = mpull($inlines, 'newInlineCommentObject');

View file

@ -328,6 +328,13 @@ final class DifferentialRevisionEditEngine
$content = array(); $content = array();
if ($inlines) { if ($inlines) {
// Reload inlines to get inline context.
$inlines = id(new DifferentialDiffInlineCommentQuery())
->setViewer($viewer)
->withIDs(mpull($inlines, 'getID'))
->needInlineContext(true)
->execute();
$inline_preview = id(new PHUIDiffInlineCommentPreviewListView()) $inline_preview = id(new PHUIDiffInlineCommentPreviewListView())
->setViewer($viewer) ->setViewer($viewer)
->setInlineComments($inlines); ->setInlineComments($inlines);

View file

@ -18,6 +18,7 @@ final class DifferentialTransactionComment
private $replyToComment = self::ATTACHABLE; private $replyToComment = self::ATTACHABLE;
private $isHidden = self::ATTACHABLE; private $isHidden = self::ATTACHABLE;
private $changeset = self::ATTACHABLE; private $changeset = self::ATTACHABLE;
private $inlineContext = self::ATTACHABLE;
public function getApplicationTransactionObject() { public function getApplicationTransactionObject() {
return new DifferentialTransaction(); return new DifferentialTransaction();
@ -129,12 +130,18 @@ final class DifferentialTransactionComment
return $this; return $this;
} }
public function isEmptyInlineComment() {
return !strlen($this->getContent());
}
public function newInlineCommentObject() { public function newInlineCommentObject() {
return DifferentialInlineComment::newFromModernComment($this); return DifferentialInlineComment::newFromModernComment($this);
} }
public function getInlineContext() {
return $this->assertAttached($this->inlineContext);
}
public function attachInlineContext(
PhabricatorInlineCommentContext $context = null) {
$this->inlineContext = $context;
return $this;
}
} }

View file

@ -373,6 +373,9 @@ final class DifferentialChangesetListView extends AphrontView {
'Add new inline comment on selected source text.' => 'Add new inline comment on selected source text.' =>
pht('Add new inline comment on selected source text.'), pht('Add new inline comment on selected source text.'),
'Suggest Edit' => pht('Suggest Edit'),
'Discard Edit' => pht('Discard Edit'),
), ),
)); ));

View file

@ -240,7 +240,7 @@ abstract class PhabricatorInlineCommentController
$view = $this->buildScaffoldForView($edit_dialog); $view = $this->buildScaffoldForView($edit_dialog);
return $this->newInlineResponse($inline, $view); return $this->newInlineResponse($inline, $view, true);
case 'cancel': case 'cancel':
$inline = $this->loadCommentByIDForEdit($this->getCommentID()); $inline = $this->loadCommentByIDForEdit($this->getCommentID());
@ -325,6 +325,9 @@ abstract class PhabricatorInlineCommentController
$this->saveComment($inline); $this->saveComment($inline);
// Reload the inline to attach context.
$inline = $this->loadCommentByIDForEdit($inline->getID());
$edit_dialog = $this->buildEditDialog($inline); $edit_dialog = $this->buildEditDialog($inline);
if ($this->getOperation() == 'reply') { if ($this->getOperation() == 'reply') {
@ -335,7 +338,7 @@ abstract class PhabricatorInlineCommentController
$view = $this->buildScaffoldForView($edit_dialog); $view = $this->buildScaffoldForView($edit_dialog);
return $this->newInlineResponse($inline, $view); return $this->newInlineResponse($inline, $view, true);
} }
} }
@ -431,7 +434,7 @@ abstract class PhabricatorInlineCommentController
$view = $this->buildScaffoldForView($view); $view = $this->buildScaffoldForView($view);
return $this->newInlineResponse($inline, $view); return $this->newInlineResponse($inline, $view, false);
} }
private function buildScaffoldForView(PHUIDiffInlineCommentView $view) { private function buildScaffoldForView(PHUIDiffInlineCommentView $view) {
@ -446,11 +449,29 @@ abstract class PhabricatorInlineCommentController
private function newInlineResponse( private function newInlineResponse(
PhabricatorInlineComment $inline, PhabricatorInlineComment $inline,
$view) { $view,
$is_edit) {
if ($inline->getReplyToCommentPHID()) {
$can_suggest = false;
} else {
$can_suggest = (bool)$inline->getInlineContext();
}
if ($is_edit) {
$viewer = $this->getViewer();
$content_state = $inline->getContentStateForEdit($viewer);
} else {
$content_state = $inline->getContentState();
}
$state_map = $content_state->newStorageMap();
$response = array( $response = array(
'inline' => array( 'inline' => array(
'id' => $inline->getID(), 'id' => $inline->getID(),
'contentState' => $state_map,
'canSuggestEdit' => $can_suggest,
), ),
'view' => hsprintf('%s', $view), 'view' => hsprintf('%s', $view),
); );
@ -477,7 +498,8 @@ abstract class PhabricatorInlineCommentController
$viewer = $this->getViewer(); $viewer = $this->getViewer();
$query = $this->newInlineCommentQuery() $query = $this->newInlineCommentQuery()
->withIDs(array($id)); ->withIDs(array($id))
->needInlineContext(true);
$inline = $this->loadCommentByQuery($query); $inline = $this->loadCommentByQuery($query);

View file

@ -12,7 +12,7 @@ final class PhabricatorDiffInlineCommentContentState
} }
if ($this->getContentHasSuggestion()) { if ($this->getContentHasSuggestion()) {
if (strlen($this->getSuggestionText())) { if (strlen($this->getContentSuggestionText())) {
return false; return false;
} }
} }

View file

@ -0,0 +1,37 @@
<?php
final class PhabricatorDiffInlineCommentContext
extends PhabricatorInlineCommentContext {
private $headLines;
private $bodyLines;
private $tailLines;
public function setHeadLines(array $head_lines) {
$this->headLines = $head_lines;
return $this;
}
public function getHeadLines() {
return $this->headLines;
}
public function setBodyLines(array $body_lines) {
$this->bodyLines = $body_lines;
return $this;
}
public function getBodyLines() {
return $this->bodyLines;
}
public function setTailLines(array $tail_lines) {
$this->tailLines = $tail_lines;
return $this;
}
public function getTailLines() {
return $this->tailLines;
}
}

View file

@ -0,0 +1,4 @@
<?php
abstract class PhabricatorInlineCommentContext
extends Phobject {}

View file

@ -350,6 +350,10 @@ abstract class PhabricatorInlineComment
return $this; return $this;
} }
public function getInlineContext() {
return $this->getStorageObject()->getInlineContext();
}
/* -( PhabricatorMarkupInterface Implementation )-------------------------- */ /* -( PhabricatorMarkupInterface Implementation )-------------------------- */

View file

@ -9,6 +9,7 @@ abstract class PhabricatorDiffInlineCommentQuery
private $publishableComments; private $publishableComments;
private $needHidden; private $needHidden;
private $needAppliedDrafts; private $needAppliedDrafts;
private $needInlineContext;
abstract protected function buildInlineCommentWhereClauseParts( abstract protected function buildInlineCommentWhereClauseParts(
AphrontDatabaseConnection $conn); AphrontDatabaseConnection $conn);
@ -42,6 +43,11 @@ abstract class PhabricatorDiffInlineCommentQuery
return $this; return $this;
} }
final public function needInlineContext($need_context) {
$this->needInlineContext = $need_context;
return $this;
}
final public function needAppliedDrafts($need_applied) { final public function needAppliedDrafts($need_applied) {
$this->needAppliedDrafts = $need_applied; $this->needAppliedDrafts = $need_applied;
return $this; return $this;
@ -173,26 +179,6 @@ abstract class PhabricatorDiffInlineCommentQuery
return $inlines; return $inlines;
} }
if ($this->needHidden) {
$viewer_phid = $viewer->getPHID();
if ($viewer_phid) {
$hidden = $this->loadHiddenCommentIDs(
$viewer_phid,
$inlines);
} else {
$hidden = array();
}
foreach ($inlines as $inline) {
$inline->attachIsHidden(isset($hidden[$inline->getID()]));
}
}
if (!$inlines) {
return $inlines;
}
$need_drafts = $this->needAppliedDrafts; $need_drafts = $this->needAppliedDrafts;
$drop_void = $this->publishableComments; $drop_void = $this->publishableComments;
$convert_objects = ($need_drafts || $drop_void); $convert_objects = ($need_drafts || $drop_void);
@ -247,4 +233,133 @@ abstract class PhabricatorDiffInlineCommentQuery
return $inlines; return $inlines;
} }
protected function didFilterPage(array $inlines) {
$viewer = $this->getViewer();
if ($this->needHidden) {
$viewer_phid = $viewer->getPHID();
if ($viewer_phid) {
$hidden = $this->loadHiddenCommentIDs(
$viewer_phid,
$inlines);
} else {
$hidden = array();
}
foreach ($inlines as $inline) {
$inline->attachIsHidden(isset($hidden[$inline->getID()]));
}
}
if ($this->needInlineContext) {
$need_context = array();
foreach ($inlines as $inline) {
$object = $inline->newInlineCommentObject();
if ($object->getDocumentEngineKey() !== null) {
$inline->attachInlineContext(null);
continue;
}
$need_context[] = $inline;
}
foreach ($need_context as $inline) {
$changeset = id(new DifferentialChangesetQuery())
->setViewer($viewer)
->withIDs(array($inline->getChangesetID()))
->needHunks(true)
->executeOne();
if (!$changeset) {
$inline->attachInlineContext(null);
continue;
}
$hunks = $changeset->getHunks();
$is_simple =
(count($hunks) === 1) &&
((int)head($hunks)->getOldOffset() <= 1) &&
((int)head($hunks)->getNewOffset() <= 1);
if (!$is_simple) {
$inline->attachInlineContext(null);
continue;
}
if ($inline->getIsNewFile()) {
$corpus = $changeset->makeNewFile();
} else {
$corpus = $changeset->makeOldFile();
}
$corpus = phutil_split_lines($corpus);
// Adjust the line number into a 0-based offset.
$offset = $inline->getLineNumber();
$offset = $offset - 1;
// Adjust the inclusive range length into a row count.
$length = $inline->getLineLength();
$length = $length + 1;
$head_min = max(0, $offset - 3);
$head_max = $offset;
$head_len = $head_max - $head_min;
if ($head_len) {
$head = array_slice($corpus, $head_min, $head_len, true);
$head = $this->simplifyContext($head, true);
} else {
$head = array();
}
$body = array_slice($corpus, $offset, $length, true);
$tail = array_slice($corpus, $offset + $length, 3, true);
$tail = $this->simplifyContext($tail, false);
$context = id(new PhabricatorDiffInlineCommentContext())
->setHeadLines($head)
->setBodyLines($body)
->setTailLines($tail);
$inline->attachInlineContext($context);
}
}
return $inlines;
}
private function simplifyContext(array $lines, $is_head) {
// We want to provide the smallest amount of context we can while still
// being useful, since the actual code is visible nearby and showing a
// ton of context is silly.
// Examine each line until we find one that looks "useful" (not just
// whitespace or a single bracket). Once we find a useful piece of context
// to anchor the text, discard the rest of the lines beyond it.
if ($is_head) {
$lines = array_reverse($lines, true);
}
$saw_context = false;
foreach ($lines as $key => $line) {
if ($saw_context) {
unset($lines[$key]);
continue;
}
$saw_context = (strlen(trim($line)) > 3);
}
if ($is_head) {
$lines = array_reverse($lines, true);
}
return $lines;
}
} }

View file

@ -427,6 +427,15 @@ final class PHUIDiffInlineCommentDetailView
$metadata['menuItems'] = $menu_items; $metadata['menuItems'] = $menu_items;
$suggestion_content = $this->newSuggestionView($inline);
$inline_content = phutil_tag(
'div',
array(
'class' => 'phabricator-remarkup',
),
$content);
$markup = javelin_tag( $markup = javelin_tag(
'div', 'div',
array( array(
@ -445,9 +454,15 @@ final class PHUIDiffInlineCommentDetailView
$group_left, $group_left,
$group_right, $group_right,
)), )),
phutil_tag_div( phutil_tag(
'differential-inline-comment-content', 'div',
phutil_tag_div('phabricator-remarkup', $content)), array(
'class' => 'differential-inline-comment-content',
),
array(
$suggestion_content,
$inline_content,
)),
)); ));
$summary = phutil_tag( $summary = phutil_tag(
@ -491,4 +506,57 @@ final class PHUIDiffInlineCommentDetailView
return true; return true;
} }
private function newSuggestionView(PhabricatorInlineComment $inline) {
$content_state = $inline->getContentState();
if (!$content_state->getContentHasSuggestion()) {
return null;
}
$context = $inline->getInlineContext();
if (!$context) {
return null;
}
$head_lines = $context->getHeadLines();
$head_lines = implode('', $head_lines);
$tail_lines = $context->getTailLines();
$tail_lines = implode('', $tail_lines);
$old_lines = $context->getBodyLines();
$old_lines = implode('', $old_lines);
$old_lines = $head_lines.$old_lines.$tail_lines;
if (strlen($old_lines) && !preg_match('/\n\z/', $old_lines)) {
$old_lines .= "\n";
}
$new_lines = $content_state->getContentSuggestionText();
$new_lines = $head_lines.$new_lines.$tail_lines;
if (strlen($new_lines) && !preg_match('/\n\z/', $new_lines)) {
$new_lines .= "\n";
}
if ($old_lines === $new_lines) {
return null;
}
$raw_diff = id(new PhabricatorDifferenceEngine())
->generateRawDiffFromFileContent($old_lines, $new_lines);
$raw_diff = phutil_split_lines($raw_diff);
$raw_diff = array_slice($raw_diff, 3);
$raw_diff = implode('', $raw_diff);
$view = phutil_tag(
'div',
array(
'class' => 'inline-suggestion-view PhabricatorMonospaced',
),
$raw_diff);
return $view;
}
} }

View file

@ -46,17 +46,23 @@ final class PHUIDiffInlineCommentEditView
), ),
$this->title); $this->title);
$corpus_view = $this->newCorpusView();
$body = phutil_tag( $body = phutil_tag(
'div', 'div',
array( array(
'class' => 'differential-inline-comment-edit-body', 'class' => 'differential-inline-comment-edit-body',
), ),
$this->newTextarea()); array(
$corpus_view,
$this->newTextarea(),
));
$edit = phutil_tag( $edit = javelin_tag(
'div', 'div',
array( array(
'class' => 'differential-inline-comment-edit-buttons grouped', 'class' => 'differential-inline-comment-edit-buttons grouped',
'sigil' => 'inline-edit-buttons',
), ),
array( array(
$buttons, $buttons,
@ -91,4 +97,121 @@ final class PHUIDiffInlineCommentEditView
->setDisableFullScreen(true); ->setDisableFullScreen(true);
} }
private function newCorpusView() {
$viewer = $this->getViewer();
$inline = $this->getInlineComment();
$context = $inline->getInlineContext();
if ($context === null) {
return null;
}
$head = $context->getHeadLines();
$head = $this->newContextView($head);
$state = $inline->getContentStateForEdit($viewer);
$main = $state->getContentSuggestionText();
$main_count = count(phutil_split_lines($main));
$default = $context->getBodyLines();
$default = implode('', $default);
// Browsers ignore one leading newline in text areas. Add one so that
// any actual leading newlines in the content are preserved.
$main = "\n".$main;
$textarea = javelin_tag(
'textarea',
array(
'class' => 'inline-suggestion-input PhabricatorMonospaced',
'rows' => max(3, $main_count + 1),
'sigil' => 'inline-content-suggestion',
'meta' => array(
'defaultText' => $default,
),
),
$main);
$main = phutil_tag(
'tr',
array(
'class' => 'inline-suggestion-input-row',
),
array(
phutil_tag(
'td',
array(
'class' => 'inline-suggestion-line-cell',
),
null),
phutil_tag(
'td',
array(
'class' => 'inline-suggestion-input-cell',
),
$textarea),
));
$tail = $context->getTailLines();
$tail = $this->newContextView($tail);
$body = phutil_tag(
'tbody',
array(),
array(
$head,
$main,
$tail,
));
$table = phutil_tag(
'table',
array(
'class' => 'inline-suggestion-table',
),
$body);
$container = phutil_tag(
'div',
array(
'class' => 'inline-suggestion',
),
$table);
return $container;
}
private function newContextView(array $lines) {
if (!$lines) {
return array();
}
$rows = array();
foreach ($lines as $index => $line) {
$line_cell = phutil_tag(
'td',
array(
'class' => 'inline-suggestion-line-cell PhabricatorMonospaced',
),
$index + 1);
$text_cell = phutil_tag(
'td',
array(
'class' => 'inline-suggestion-text-cell PhabricatorMonospaced',
),
$line);
$cells = array(
$line_cell,
$text_cell,
);
$rows[] = phutil_tag('tr', array(), $cells);
}
return $rows;
}
} }

View file

@ -93,7 +93,7 @@ abstract class PHUIDiffInlineCommentView extends AphrontView {
'startOffset' => $inline->getStartOffset(), 'startOffset' => $inline->getStartOffset(),
'endOffset' => $inline->getEndOffset(), 'endOffset' => $inline->getEndOffset(),
'on_right' => $this->getIsOnRight(), 'on_right' => $this->getIsOnRight(),
'contentState' => $inline->getContentState(), 'contentState' => $inline->getContentState()->newStorageMap(),
); );
} }

View file

@ -436,3 +436,60 @@
background: {$lightyellow}; background: {$lightyellow};
border-color: {$yellow}; border-color: {$yellow};
} }
.inline-suggestion {
display: none;
margin: 0 -8px;
}
.has-suggestion .inline-suggestion {
display: block;
}
.differential-inline-comment-edit-buttons button.inline-button-left {
float: left;
margin: 0 6px 0 0;
}
.inline-suggestion-table {
table-layout: fixed;
width: 100%;
margin-bottom: 8px;
white-space: pre-wrap;
background: {$greybackground};
border-width: 1px 0;
border-style: solid;
border-color: {$lightgreyborder};
}
textarea.inline-suggestion-input {
width: 100%;
height: auto;
max-width: 100%;
}
.inline-suggestion-line-cell {
text-align: right;
background: {$darkgreybackground};
width: 36px;
color: {$greytext};
border-right: 1px solid {$lightgreyborder};
}
.inline-suggestion-input-cell {
padding: 8px;
}
.inline-suggestion-text-cell {
padding: 0 8px;
}
.inline-suggestion-view {
padding: 8px 12px;
white-space: pre-wrap;
background: {$greybackground};
margin: 0 -12px 8px;
border-width: 1px 0;
border-style: solid;
border-color: {$lightgreyborder};
}

View file

@ -51,6 +51,7 @@ JX.install('DiffInline', {
_startOffset: null, _startOffset: null,
_endOffset: null, _endOffset: null,
_isSelected: false, _isSelected: false,
_canSuggestEdit: false,
bindToRow: function(row) { bindToRow: function(row) {
this._row = row; this._row = row;
@ -76,7 +77,6 @@ JX.install('DiffInline', {
this._number = parseInt(data.number, 10); this._number = parseInt(data.number, 10);
this._length = parseInt(data.length, 10); this._length = parseInt(data.length, 10);
this._originalState = data.contentState;
this._isNewFile = data.isNewFile; this._isNewFile = data.isNewFile;
this._replyToCommentPHID = data.replyToCommentPHID; this._replyToCommentPHID = data.replyToCommentPHID;
@ -602,6 +602,8 @@ JX.install('DiffInline', {
_readInlineState: function(state) { _readInlineState: function(state) {
this._id = state.id; this._id = state.id;
this._originalState = state.contentState;
this._canSuggestEdit = state.canSuggestEdit;
}, },
_ondeleteresponse: function() { _ondeleteresponse: function() {
@ -664,6 +666,11 @@ JX.install('DiffInline', {
_drawEditRows: function(rows) { _drawEditRows: function(rows) {
this.setEditing(true); this.setEditing(true);
this._editRow = this._drawRows(rows, null, 'edit'); this._editRow = this._drawRows(rows, null, 'edit');
this._drawSuggestionState(this._editRow);
JX.log(this._originalState);
this.setHasSuggestion(this._originalState.hasSuggestion);
}, },
_drawRows: function(rows, cursor, type) { _drawRows: function(rows, cursor, type) {
@ -719,6 +726,91 @@ JX.install('DiffInline', {
return result_row; return result_row;
}, },
_drawSuggestionState: function(row) {
if (this._canSuggestEdit) {
var button = this._getSuggestionButton();
var node = button.getNode();
// As a side effect of form submission, the button may become
// visually disabled. Re-enable it. This is a bit hacky.
JX.DOM.alterClass(node, 'disabled', false);
node.disabled = false;
var container = JX.DOM.find(row, 'div', 'inline-edit-buttons');
container.appendChild(node);
}
},
_getSuggestionButton: function() {
if (!this._suggestionButton) {
var button = new JX.PHUIXButtonView()
.setIcon('fa-pencil-square-o')
.setColor('grey');
var node = button.getNode();
JX.DOM.alterClass(node, 'inline-button-left', true);
var onclick = JX.bind(this, this._onSuggestEdit);
JX.DOM.listen(node, 'click', null, onclick);
this._suggestionButton = button;
}
return this._suggestionButton;
},
_onSuggestEdit: function(e) {
e.kill();
this.setHasSuggestion(!this.getHasSuggestion());
// The first time the user actually clicks the button and enables
// suggestions for a given editor state, fill the input with the
// underlying text if there isn't any text yet.
if (this.getHasSuggestion()) {
if (this._editRow) {
var node = this._getSuggestionNode(this._editRow);
if (node) {
if (!node.value.length) {
var data = JX.Stratcom.getData(node);
if (!data.hasSetDefault) {
data.hasSetDefault = true;
node.value = data.defaultText;
node.rows = Math.max(3, node.value.split('\n').length);
}
}
}
}
}
// Save the "hasSuggestion" part of the content state.
this.triggerDraft();
},
setHasSuggestion: function(has_suggestion) {
this._hasSuggestion = has_suggestion;
var button = this._getSuggestionButton();
var pht = this.getChangeset().getChangesetList().getTranslations();
if (has_suggestion) {
button
.setIcon('fa-times')
.setText(pht('Discard Edit'));
} else {
button
.setIcon('fa-plus')
.setText(pht('Suggest Edit'));
}
if (this._editRow) {
JX.DOM.alterClass(this._editRow, 'has-suggestion', has_suggestion);
}
},
getHasSuggestion: function() {
return this._hasSuggestion;
},
save: function() { save: function() {
var handler = JX.bind(this, this._onsubmitresponse); var handler = JX.bind(this, this._onsubmitresponse);
@ -825,16 +917,24 @@ JX.install('DiffInline', {
// Ignore. // Ignore.
} }
try { node = this._getSuggestionNode(row);
node = JX.DOM.find(row, 'textarea', 'inline-content-suggestion'); if (node) {
state.suggestionText = node.value; state.suggestionText = node.value;
} catch (ex) {
// Ignore.
} }
state.hasSuggestion = this.getHasSuggestion();
return state; return state;
}, },
_getSuggestionNode: function(row) {
try {
return JX.DOM.find(row, 'textarea', 'inline-content-suggestion');
} catch (ex) {
return null;
}
},
_onsubmitresponse: function(response) { _onsubmitresponse: function(response) {
if (this._editRow) { if (this._editRow) {
JX.DOM.remove(this._editRow); JX.DOM.remove(this._editRow);
@ -1063,7 +1163,7 @@ JX.install('DiffInline', {
return { return {
text: '', text: '',
suggestionText: '', suggestionText: '',
hasSuggestion: true hasSuggestion: false
}; };
}, },
@ -1073,6 +1173,7 @@ JX.install('DiffInline', {
_isSameContentState: function(u, v) { _isSameContentState: function(u, v) {
return ( return (
((u === null) === (v === null)) &&
(u.text === v.text) && (u.text === v.text) &&
(u.suggestionText === v.suggestionText) && (u.suggestionText === v.suggestionText) &&
(u.hasSuggestion === v.hasSuggestion)); (u.hasSuggestion === v.hasSuggestion));