@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

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

+615 -55
+9 -9
resources/celerity/map.php
··· 12 12 'core.pkg.css' => 'ba768cdb', 13 13 'core.pkg.js' => '845355f4', 14 14 'dark-console.pkg.js' => '187792c2', 15 - 'differential.pkg.css' => '42a2334f', 16 - 'differential.pkg.js' => 'd0ddfb19', 15 + 'differential.pkg.css' => 'f924dbcf', 16 + 'differential.pkg.js' => '256a327a', 17 17 'diffusion.pkg.css' => '42c75c37', 18 18 'diffusion.pkg.js' => 'a98c0bf7', 19 19 'maniphest.pkg.css' => '35995d6d', ··· 65 65 'rsrc/css/application/differential/add-comment.css' => '7e5900d9', 66 66 'rsrc/css/application/differential/changeset-view.css' => '60c3d405', 67 67 'rsrc/css/application/differential/core.css' => '7300a73e', 68 - 'rsrc/css/application/differential/phui-inline-comment.css' => 'd5749acc', 68 + 'rsrc/css/application/differential/phui-inline-comment.css' => '4107254a', 69 69 'rsrc/css/application/differential/revision-comment.css' => '7dbc8d1d', 70 70 'rsrc/css/application/differential/revision-history.css' => '8aa3eac5', 71 71 'rsrc/css/application/differential/revision-list.css' => '93d2df7d', ··· 381 381 'rsrc/js/application/dashboard/behavior-dashboard-tab-panel.js' => '0116d3e8', 382 382 'rsrc/js/application/diff/DiffChangeset.js' => '6e5e03d2', 383 383 'rsrc/js/application/diff/DiffChangesetList.js' => 'b51ba93a', 384 - 'rsrc/js/application/diff/DiffInline.js' => '6fa445ef', 384 + 'rsrc/js/application/diff/DiffInline.js' => '829b88bf', 385 385 'rsrc/js/application/diff/DiffPathView.js' => '8207abf9', 386 386 'rsrc/js/application/diff/DiffTreeView.js' => '5d83623b', 387 387 'rsrc/js/application/differential/behavior-diff-radios.js' => '925fe8cd', ··· 776 776 'phabricator-dashboard-css' => '5a205b9d', 777 777 'phabricator-diff-changeset' => '6e5e03d2', 778 778 'phabricator-diff-changeset-list' => 'b51ba93a', 779 - 'phabricator-diff-inline' => '6fa445ef', 779 + 'phabricator-diff-inline' => '829b88bf', 780 780 'phabricator-diff-path-view' => '8207abf9', 781 781 'phabricator-diff-tree-view' => '5d83623b', 782 782 'phabricator-drag-and-drop-file-upload' => '4370900d', ··· 854 854 'phui-icon-view-css' => '4cbc684a', 855 855 'phui-image-mask-css' => '62c7f4d2', 856 856 'phui-info-view-css' => 'a10a909b', 857 - 'phui-inline-comment-view-css' => 'd5749acc', 857 + 'phui-inline-comment-view-css' => '4107254a', 858 858 'phui-invisible-character-view-css' => 'c694c4a4', 859 859 'phui-left-right-css' => '68513c34', 860 860 'phui-lightbox-css' => '4ebf22da', ··· 1561 1561 'phabricator-diff-path-view', 1562 1562 'phuix-button-view', 1563 1563 ), 1564 - '6fa445ef' => array( 1565 - 'javelin-dom', 1566 - ), 1567 1564 70245195 => array( 1568 1565 'javelin-behavior', 1569 1566 'javelin-stratcom', ··· 1640 1637 'javelin-vector', 1641 1638 ), 1642 1639 '8207abf9' => array( 1640 + 'javelin-dom', 1641 + ), 1642 + '829b88bf' => array( 1643 1643 'javelin-dom', 1644 1644 ), 1645 1645 83754533 => array(
+4
src/__phutil_library_map__.php
··· 3161 3161 'PhabricatorDeveloperConfigOptions' => 'applications/config/option/PhabricatorDeveloperConfigOptions.php', 3162 3162 'PhabricatorDeveloperPreferencesSettingsPanel' => 'applications/settings/panel/PhabricatorDeveloperPreferencesSettingsPanel.php', 3163 3163 'PhabricatorDiffInlineCommentContentState' => 'infrastructure/diff/inline/PhabricatorDiffInlineCommentContentState.php', 3164 + 'PhabricatorDiffInlineCommentContext' => 'infrastructure/diff/inline/PhabricatorDiffInlineCommentContext.php', 3164 3165 'PhabricatorDiffInlineCommentQuery' => 'infrastructure/diff/query/PhabricatorDiffInlineCommentQuery.php', 3165 3166 'PhabricatorDiffPreferencesSettingsPanel' => 'applications/settings/panel/PhabricatorDiffPreferencesSettingsPanel.php', 3166 3167 'PhabricatorDiffScopeEngine' => 'infrastructure/diff/PhabricatorDiffScopeEngine.php', ··· 3594 3595 'PhabricatorInlineComment' => 'infrastructure/diff/interface/PhabricatorInlineComment.php', 3595 3596 'PhabricatorInlineCommentAdjustmentEngine' => 'infrastructure/diff/engine/PhabricatorInlineCommentAdjustmentEngine.php', 3596 3597 'PhabricatorInlineCommentContentState' => 'infrastructure/diff/inline/PhabricatorInlineCommentContentState.php', 3598 + 'PhabricatorInlineCommentContext' => 'infrastructure/diff/inline/PhabricatorInlineCommentContext.php', 3597 3599 'PhabricatorInlineCommentController' => 'infrastructure/diff/PhabricatorInlineCommentController.php', 3598 3600 'PhabricatorInlineCommentInterface' => 'applications/transactions/interface/PhabricatorInlineCommentInterface.php', 3599 3601 'PhabricatorInlineSummaryView' => 'infrastructure/diff/view/PhabricatorInlineSummaryView.php', ··· 9630 9632 'PhabricatorDeveloperConfigOptions' => 'PhabricatorApplicationConfigOptions', 9631 9633 'PhabricatorDeveloperPreferencesSettingsPanel' => 'PhabricatorEditEngineSettingsPanel', 9632 9634 'PhabricatorDiffInlineCommentContentState' => 'PhabricatorInlineCommentContentState', 9635 + 'PhabricatorDiffInlineCommentContext' => 'PhabricatorInlineCommentContext', 9633 9636 'PhabricatorDiffInlineCommentQuery' => 'PhabricatorApplicationTransactionCommentQuery', 9634 9637 'PhabricatorDiffPreferencesSettingsPanel' => 'PhabricatorEditEngineSettingsPanel', 9635 9638 'PhabricatorDiffScopeEngine' => 'Phobject', ··· 10122 10125 ), 10123 10126 'PhabricatorInlineCommentAdjustmentEngine' => 'Phobject', 10124 10127 'PhabricatorInlineCommentContentState' => 'Phobject', 10128 + 'PhabricatorInlineCommentContext' => 'Phobject', 10125 10129 'PhabricatorInlineCommentController' => 'PhabricatorController', 10126 10130 'PhabricatorInlineSummaryView' => 'AphrontView', 10127 10131 'PhabricatorInstructionsEditField' => 'PhabricatorEditField',
+11 -4
src/applications/audit/storage/PhabricatorAuditTransactionComment.php
··· 17 17 protected $attributes = array(); 18 18 19 19 private $replyToComment = self::ATTACHABLE; 20 + private $inlineContext = self::ATTACHABLE; 20 21 21 22 public function getApplicationTransactionObject() { 22 23 return new PhabricatorAuditTransaction(); ··· 83 84 return $this; 84 85 } 85 86 86 - public function isEmptyInlineComment() { 87 - return !strlen($this->getContent()); 87 + public function newInlineCommentObject() { 88 + return PhabricatorAuditInlineComment::newFromModernComment($this); 88 89 } 89 90 90 - public function newInlineCommentObject() { 91 - return PhabricatorAuditInlineComment::newFromModernComment($this); 91 + public function getInlineContext() { 92 + return $this->assertAttached($this->inlineContext); 93 + } 94 + 95 + public function attachInlineContext( 96 + PhabricatorInlineCommentContext $context = null) { 97 + $this->inlineContext = $context; 98 + return $this; 92 99 } 93 100 94 101 }
+1
src/applications/differential/controller/DifferentialChangesetViewController.php
··· 200 200 ->withPublishableComments(true) 201 201 ->withPublishedComments(true) 202 202 ->needHidden(true) 203 + ->needInlineContext(true) 203 204 ->execute(); 204 205 205 206 $inlines = mpull($inlines, 'newInlineCommentObject');
+7
src/applications/differential/editor/DifferentialRevisionEditEngine.php
··· 328 328 $content = array(); 329 329 330 330 if ($inlines) { 331 + // Reload inlines to get inline context. 332 + $inlines = id(new DifferentialDiffInlineCommentQuery()) 333 + ->setViewer($viewer) 334 + ->withIDs(mpull($inlines, 'getID')) 335 + ->needInlineContext(true) 336 + ->execute(); 337 + 331 338 $inline_preview = id(new PHUIDiffInlineCommentPreviewListView()) 332 339 ->setViewer($viewer) 333 340 ->setInlineComments($inlines);
+11 -4
src/applications/differential/storage/DifferentialTransactionComment.php
··· 18 18 private $replyToComment = self::ATTACHABLE; 19 19 private $isHidden = self::ATTACHABLE; 20 20 private $changeset = self::ATTACHABLE; 21 + private $inlineContext = self::ATTACHABLE; 21 22 22 23 public function getApplicationTransactionObject() { 23 24 return new DifferentialTransaction(); ··· 129 130 return $this; 130 131 } 131 132 132 - public function isEmptyInlineComment() { 133 - return !strlen($this->getContent()); 133 + public function newInlineCommentObject() { 134 + return DifferentialInlineComment::newFromModernComment($this); 134 135 } 135 136 136 - public function newInlineCommentObject() { 137 - return DifferentialInlineComment::newFromModernComment($this); 137 + public function getInlineContext() { 138 + return $this->assertAttached($this->inlineContext); 139 + } 140 + 141 + public function attachInlineContext( 142 + PhabricatorInlineCommentContext $context = null) { 143 + $this->inlineContext = $context; 144 + return $this; 138 145 } 139 146 140 147 }
+3
src/applications/differential/view/DifferentialChangesetListView.php
··· 373 373 374 374 'Add new inline comment on selected source text.' => 375 375 pht('Add new inline comment on selected source text.'), 376 + 377 + 'Suggest Edit' => pht('Suggest Edit'), 378 + 'Discard Edit' => pht('Discard Edit'), 376 379 ), 377 380 )); 378 381
+27 -5
src/infrastructure/diff/PhabricatorInlineCommentController.php
··· 240 240 241 241 $view = $this->buildScaffoldForView($edit_dialog); 242 242 243 - return $this->newInlineResponse($inline, $view); 243 + return $this->newInlineResponse($inline, $view, true); 244 244 case 'cancel': 245 245 $inline = $this->loadCommentByIDForEdit($this->getCommentID()); 246 246 ··· 325 325 326 326 $this->saveComment($inline); 327 327 328 + // Reload the inline to attach context. 329 + $inline = $this->loadCommentByIDForEdit($inline->getID()); 330 + 328 331 $edit_dialog = $this->buildEditDialog($inline); 329 332 330 333 if ($this->getOperation() == 'reply') { ··· 335 338 336 339 $view = $this->buildScaffoldForView($edit_dialog); 337 340 338 - return $this->newInlineResponse($inline, $view); 341 + return $this->newInlineResponse($inline, $view, true); 339 342 } 340 343 } 341 344 ··· 431 434 432 435 $view = $this->buildScaffoldForView($view); 433 436 434 - return $this->newInlineResponse($inline, $view); 437 + return $this->newInlineResponse($inline, $view, false); 435 438 } 436 439 437 440 private function buildScaffoldForView(PHUIDiffInlineCommentView $view) { ··· 446 449 447 450 private function newInlineResponse( 448 451 PhabricatorInlineComment $inline, 449 - $view) { 452 + $view, 453 + $is_edit) { 454 + 455 + if ($inline->getReplyToCommentPHID()) { 456 + $can_suggest = false; 457 + } else { 458 + $can_suggest = (bool)$inline->getInlineContext(); 459 + } 460 + 461 + if ($is_edit) { 462 + $viewer = $this->getViewer(); 463 + $content_state = $inline->getContentStateForEdit($viewer); 464 + } else { 465 + $content_state = $inline->getContentState(); 466 + } 467 + 468 + $state_map = $content_state->newStorageMap(); 450 469 451 470 $response = array( 452 471 'inline' => array( 453 472 'id' => $inline->getID(), 473 + 'contentState' => $state_map, 474 + 'canSuggestEdit' => $can_suggest, 454 475 ), 455 476 'view' => hsprintf('%s', $view), 456 477 ); ··· 477 498 $viewer = $this->getViewer(); 478 499 479 500 $query = $this->newInlineCommentQuery() 480 - ->withIDs(array($id)); 501 + ->withIDs(array($id)) 502 + ->needInlineContext(true); 481 503 482 504 $inline = $this->loadCommentByQuery($query); 483 505
+1 -1
src/infrastructure/diff/inline/PhabricatorDiffInlineCommentContentState.php
··· 12 12 } 13 13 14 14 if ($this->getContentHasSuggestion()) { 15 - if (strlen($this->getSuggestionText())) { 15 + if (strlen($this->getContentSuggestionText())) { 16 16 return false; 17 17 } 18 18 }
+37
src/infrastructure/diff/inline/PhabricatorDiffInlineCommentContext.php
··· 1 + <?php 2 + 3 + final class PhabricatorDiffInlineCommentContext 4 + extends PhabricatorInlineCommentContext { 5 + 6 + private $headLines; 7 + private $bodyLines; 8 + private $tailLines; 9 + 10 + public function setHeadLines(array $head_lines) { 11 + $this->headLines = $head_lines; 12 + return $this; 13 + } 14 + 15 + public function getHeadLines() { 16 + return $this->headLines; 17 + } 18 + 19 + public function setBodyLines(array $body_lines) { 20 + $this->bodyLines = $body_lines; 21 + return $this; 22 + } 23 + 24 + public function getBodyLines() { 25 + return $this->bodyLines; 26 + } 27 + 28 + public function setTailLines(array $tail_lines) { 29 + $this->tailLines = $tail_lines; 30 + return $this; 31 + } 32 + 33 + public function getTailLines() { 34 + return $this->tailLines; 35 + } 36 + 37 + }
+4
src/infrastructure/diff/inline/PhabricatorInlineCommentContext.php
··· 1 + <?php 2 + 3 + abstract class PhabricatorInlineCommentContext 4 + extends Phobject {}
+4
src/infrastructure/diff/interface/PhabricatorInlineComment.php
··· 350 350 return $this; 351 351 } 352 352 353 + public function getInlineContext() { 354 + return $this->getStorageObject()->getInlineContext(); 355 + } 356 + 353 357 354 358 /* -( PhabricatorMarkupInterface Implementation )-------------------------- */ 355 359
+135 -20
src/infrastructure/diff/query/PhabricatorDiffInlineCommentQuery.php
··· 9 9 private $publishableComments; 10 10 private $needHidden; 11 11 private $needAppliedDrafts; 12 + private $needInlineContext; 12 13 13 14 abstract protected function buildInlineCommentWhereClauseParts( 14 15 AphrontDatabaseConnection $conn); ··· 39 40 40 41 final public function needHidden($need_hidden) { 41 42 $this->needHidden = $need_hidden; 43 + return $this; 44 + } 45 + 46 + final public function needInlineContext($need_context) { 47 + $this->needInlineContext = $need_context; 42 48 return $this; 43 49 } 44 50 ··· 173 179 return $inlines; 174 180 } 175 181 176 - if ($this->needHidden) { 177 - $viewer_phid = $viewer->getPHID(); 178 - 179 - if ($viewer_phid) { 180 - $hidden = $this->loadHiddenCommentIDs( 181 - $viewer_phid, 182 - $inlines); 183 - } else { 184 - $hidden = array(); 185 - } 186 - 187 - foreach ($inlines as $inline) { 188 - $inline->attachIsHidden(isset($hidden[$inline->getID()])); 189 - } 190 - } 191 - 192 - if (!$inlines) { 193 - return $inlines; 194 - } 195 - 196 182 $need_drafts = $this->needAppliedDrafts; 197 183 $drop_void = $this->publishableComments; 198 184 $convert_objects = ($need_drafts || $drop_void); ··· 247 233 return $inlines; 248 234 } 249 235 236 + protected function didFilterPage(array $inlines) { 237 + $viewer = $this->getViewer(); 238 + 239 + if ($this->needHidden) { 240 + $viewer_phid = $viewer->getPHID(); 241 + 242 + if ($viewer_phid) { 243 + $hidden = $this->loadHiddenCommentIDs( 244 + $viewer_phid, 245 + $inlines); 246 + } else { 247 + $hidden = array(); 248 + } 249 + 250 + foreach ($inlines as $inline) { 251 + $inline->attachIsHidden(isset($hidden[$inline->getID()])); 252 + } 253 + } 254 + 255 + if ($this->needInlineContext) { 256 + $need_context = array(); 257 + foreach ($inlines as $inline) { 258 + $object = $inline->newInlineCommentObject(); 259 + 260 + if ($object->getDocumentEngineKey() !== null) { 261 + $inline->attachInlineContext(null); 262 + continue; 263 + } 264 + 265 + $need_context[] = $inline; 266 + } 267 + 268 + foreach ($need_context as $inline) { 269 + $changeset = id(new DifferentialChangesetQuery()) 270 + ->setViewer($viewer) 271 + ->withIDs(array($inline->getChangesetID())) 272 + ->needHunks(true) 273 + ->executeOne(); 274 + if (!$changeset) { 275 + $inline->attachInlineContext(null); 276 + continue; 277 + } 278 + 279 + $hunks = $changeset->getHunks(); 280 + 281 + $is_simple = 282 + (count($hunks) === 1) && 283 + ((int)head($hunks)->getOldOffset() <= 1) && 284 + ((int)head($hunks)->getNewOffset() <= 1); 285 + 286 + if (!$is_simple) { 287 + $inline->attachInlineContext(null); 288 + continue; 289 + } 290 + 291 + if ($inline->getIsNewFile()) { 292 + $corpus = $changeset->makeNewFile(); 293 + } else { 294 + $corpus = $changeset->makeOldFile(); 295 + } 296 + 297 + $corpus = phutil_split_lines($corpus); 298 + 299 + // Adjust the line number into a 0-based offset. 300 + $offset = $inline->getLineNumber(); 301 + $offset = $offset - 1; 302 + 303 + // Adjust the inclusive range length into a row count. 304 + $length = $inline->getLineLength(); 305 + $length = $length + 1; 306 + 307 + $head_min = max(0, $offset - 3); 308 + $head_max = $offset; 309 + $head_len = $head_max - $head_min; 310 + 311 + if ($head_len) { 312 + $head = array_slice($corpus, $head_min, $head_len, true); 313 + $head = $this->simplifyContext($head, true); 314 + } else { 315 + $head = array(); 316 + } 317 + 318 + $body = array_slice($corpus, $offset, $length, true); 319 + 320 + $tail = array_slice($corpus, $offset + $length, 3, true); 321 + $tail = $this->simplifyContext($tail, false); 322 + 323 + $context = id(new PhabricatorDiffInlineCommentContext()) 324 + ->setHeadLines($head) 325 + ->setBodyLines($body) 326 + ->setTailLines($tail); 327 + 328 + $inline->attachInlineContext($context); 329 + } 330 + 331 + } 332 + 333 + return $inlines; 334 + } 335 + 336 + private function simplifyContext(array $lines, $is_head) { 337 + // We want to provide the smallest amount of context we can while still 338 + // being useful, since the actual code is visible nearby and showing a 339 + // ton of context is silly. 340 + 341 + // Examine each line until we find one that looks "useful" (not just 342 + // whitespace or a single bracket). Once we find a useful piece of context 343 + // to anchor the text, discard the rest of the lines beyond it. 344 + 345 + if ($is_head) { 346 + $lines = array_reverse($lines, true); 347 + } 348 + 349 + $saw_context = false; 350 + foreach ($lines as $key => $line) { 351 + if ($saw_context) { 352 + unset($lines[$key]); 353 + continue; 354 + } 355 + 356 + $saw_context = (strlen(trim($line)) > 3); 357 + } 358 + 359 + if ($is_head) { 360 + $lines = array_reverse($lines, true); 361 + } 362 + 363 + return $lines; 364 + } 250 365 }
+71 -3
src/infrastructure/diff/view/PHUIDiffInlineCommentDetailView.php
··· 427 427 428 428 $metadata['menuItems'] = $menu_items; 429 429 430 + $suggestion_content = $this->newSuggestionView($inline); 431 + 432 + $inline_content = phutil_tag( 433 + 'div', 434 + array( 435 + 'class' => 'phabricator-remarkup', 436 + ), 437 + $content); 438 + 430 439 $markup = javelin_tag( 431 440 'div', 432 441 array( ··· 445 454 $group_left, 446 455 $group_right, 447 456 )), 448 - phutil_tag_div( 449 - 'differential-inline-comment-content', 450 - phutil_tag_div('phabricator-remarkup', $content)), 457 + phutil_tag( 458 + 'div', 459 + array( 460 + 'class' => 'differential-inline-comment-content', 461 + ), 462 + array( 463 + $suggestion_content, 464 + $inline_content, 465 + )), 451 466 )); 452 467 453 468 $summary = phutil_tag( ··· 490 505 491 506 return true; 492 507 } 508 + 509 + private function newSuggestionView(PhabricatorInlineComment $inline) { 510 + $content_state = $inline->getContentState(); 511 + if (!$content_state->getContentHasSuggestion()) { 512 + return null; 513 + } 514 + 515 + $context = $inline->getInlineContext(); 516 + if (!$context) { 517 + return null; 518 + } 519 + 520 + $head_lines = $context->getHeadLines(); 521 + $head_lines = implode('', $head_lines); 522 + 523 + $tail_lines = $context->getTailLines(); 524 + $tail_lines = implode('', $tail_lines); 525 + 526 + $old_lines = $context->getBodyLines(); 527 + $old_lines = implode('', $old_lines); 528 + $old_lines = $head_lines.$old_lines.$tail_lines; 529 + if (strlen($old_lines) && !preg_match('/\n\z/', $old_lines)) { 530 + $old_lines .= "\n"; 531 + } 532 + 533 + $new_lines = $content_state->getContentSuggestionText(); 534 + $new_lines = $head_lines.$new_lines.$tail_lines; 535 + if (strlen($new_lines) && !preg_match('/\n\z/', $new_lines)) { 536 + $new_lines .= "\n"; 537 + } 538 + 539 + if ($old_lines === $new_lines) { 540 + return null; 541 + } 542 + 543 + 544 + $raw_diff = id(new PhabricatorDifferenceEngine()) 545 + ->generateRawDiffFromFileContent($old_lines, $new_lines); 546 + 547 + $raw_diff = phutil_split_lines($raw_diff); 548 + $raw_diff = array_slice($raw_diff, 3); 549 + $raw_diff = implode('', $raw_diff); 550 + 551 + $view = phutil_tag( 552 + 'div', 553 + array( 554 + 'class' => 'inline-suggestion-view PhabricatorMonospaced', 555 + ), 556 + $raw_diff); 557 + 558 + return $view; 559 + } 560 + 493 561 494 562 }
+125 -2
src/infrastructure/diff/view/PHUIDiffInlineCommentEditView.php
··· 46 46 ), 47 47 $this->title); 48 48 49 + $corpus_view = $this->newCorpusView(); 50 + 49 51 $body = phutil_tag( 50 52 'div', 51 53 array( 52 54 'class' => 'differential-inline-comment-edit-body', 53 55 ), 54 - $this->newTextarea()); 56 + array( 57 + $corpus_view, 58 + $this->newTextarea(), 59 + )); 55 60 56 - $edit = phutil_tag( 61 + $edit = javelin_tag( 57 62 'div', 58 63 array( 59 64 'class' => 'differential-inline-comment-edit-buttons grouped', 65 + 'sigil' => 'inline-edit-buttons', 60 66 ), 61 67 array( 62 68 $buttons, ··· 89 95 ->setSigil('inline-content-text') 90 96 ->setValue($state->getContentText()) 91 97 ->setDisableFullScreen(true); 98 + } 99 + 100 + private function newCorpusView() { 101 + $viewer = $this->getViewer(); 102 + $inline = $this->getInlineComment(); 103 + 104 + $context = $inline->getInlineContext(); 105 + if ($context === null) { 106 + return null; 107 + } 108 + 109 + $head = $context->getHeadLines(); 110 + $head = $this->newContextView($head); 111 + 112 + $state = $inline->getContentStateForEdit($viewer); 113 + 114 + $main = $state->getContentSuggestionText(); 115 + $main_count = count(phutil_split_lines($main)); 116 + 117 + $default = $context->getBodyLines(); 118 + $default = implode('', $default); 119 + 120 + // Browsers ignore one leading newline in text areas. Add one so that 121 + // any actual leading newlines in the content are preserved. 122 + $main = "\n".$main; 123 + 124 + $textarea = javelin_tag( 125 + 'textarea', 126 + array( 127 + 'class' => 'inline-suggestion-input PhabricatorMonospaced', 128 + 'rows' => max(3, $main_count + 1), 129 + 'sigil' => 'inline-content-suggestion', 130 + 'meta' => array( 131 + 'defaultText' => $default, 132 + ), 133 + ), 134 + $main); 135 + 136 + $main = phutil_tag( 137 + 'tr', 138 + array( 139 + 'class' => 'inline-suggestion-input-row', 140 + ), 141 + array( 142 + phutil_tag( 143 + 'td', 144 + array( 145 + 'class' => 'inline-suggestion-line-cell', 146 + ), 147 + null), 148 + phutil_tag( 149 + 'td', 150 + array( 151 + 'class' => 'inline-suggestion-input-cell', 152 + ), 153 + $textarea), 154 + )); 155 + 156 + $tail = $context->getTailLines(); 157 + $tail = $this->newContextView($tail); 158 + 159 + $body = phutil_tag( 160 + 'tbody', 161 + array(), 162 + array( 163 + $head, 164 + $main, 165 + $tail, 166 + )); 167 + 168 + $table = phutil_tag( 169 + 'table', 170 + array( 171 + 'class' => 'inline-suggestion-table', 172 + ), 173 + $body); 174 + 175 + $container = phutil_tag( 176 + 'div', 177 + array( 178 + 'class' => 'inline-suggestion', 179 + ), 180 + $table); 181 + 182 + return $container; 183 + } 184 + 185 + private function newContextView(array $lines) { 186 + if (!$lines) { 187 + return array(); 188 + } 189 + 190 + $rows = array(); 191 + foreach ($lines as $index => $line) { 192 + $line_cell = phutil_tag( 193 + 'td', 194 + array( 195 + 'class' => 'inline-suggestion-line-cell PhabricatorMonospaced', 196 + ), 197 + $index + 1); 198 + 199 + $text_cell = phutil_tag( 200 + 'td', 201 + array( 202 + 'class' => 'inline-suggestion-text-cell PhabricatorMonospaced', 203 + ), 204 + $line); 205 + 206 + $cells = array( 207 + $line_cell, 208 + $text_cell, 209 + ); 210 + 211 + $rows[] = phutil_tag('tr', array(), $cells); 212 + } 213 + 214 + return $rows; 92 215 } 93 216 94 217 }
+1 -1
src/infrastructure/diff/view/PHUIDiffInlineCommentView.php
··· 93 93 'startOffset' => $inline->getStartOffset(), 94 94 'endOffset' => $inline->getEndOffset(), 95 95 'on_right' => $this->getIsOnRight(), 96 - 'contentState' => $inline->getContentState(), 96 + 'contentState' => $inline->getContentState()->newStorageMap(), 97 97 ); 98 98 } 99 99
+57
webroot/rsrc/css/application/differential/phui-inline-comment.css
··· 436 436 background: {$lightyellow}; 437 437 border-color: {$yellow}; 438 438 } 439 + 440 + .inline-suggestion { 441 + display: none; 442 + margin: 0 -8px; 443 + } 444 + 445 + .has-suggestion .inline-suggestion { 446 + display: block; 447 + } 448 + 449 + .differential-inline-comment-edit-buttons button.inline-button-left { 450 + float: left; 451 + margin: 0 6px 0 0; 452 + } 453 + 454 + .inline-suggestion-table { 455 + table-layout: fixed; 456 + width: 100%; 457 + margin-bottom: 8px; 458 + white-space: pre-wrap; 459 + background: {$greybackground}; 460 + border-width: 1px 0; 461 + border-style: solid; 462 + border-color: {$lightgreyborder}; 463 + } 464 + 465 + textarea.inline-suggestion-input { 466 + width: 100%; 467 + height: auto; 468 + max-width: 100%; 469 + } 470 + 471 + .inline-suggestion-line-cell { 472 + text-align: right; 473 + background: {$darkgreybackground}; 474 + width: 36px; 475 + color: {$greytext}; 476 + border-right: 1px solid {$lightgreyborder}; 477 + } 478 + 479 + .inline-suggestion-input-cell { 480 + padding: 8px; 481 + } 482 + 483 + .inline-suggestion-text-cell { 484 + padding: 0 8px; 485 + } 486 + 487 + .inline-suggestion-view { 488 + padding: 8px 12px; 489 + white-space: pre-wrap; 490 + background: {$greybackground}; 491 + margin: 0 -12px 8px; 492 + border-width: 1px 0; 493 + border-style: solid; 494 + border-color: {$lightgreyborder}; 495 + }
+107 -6
webroot/rsrc/js/application/diff/DiffInline.js
··· 51 51 _startOffset: null, 52 52 _endOffset: null, 53 53 _isSelected: false, 54 + _canSuggestEdit: false, 54 55 55 56 bindToRow: function(row) { 56 57 this._row = row; ··· 76 77 this._number = parseInt(data.number, 10); 77 78 this._length = parseInt(data.length, 10); 78 79 79 - this._originalState = data.contentState; 80 80 this._isNewFile = data.isNewFile; 81 81 82 82 this._replyToCommentPHID = data.replyToCommentPHID; ··· 602 602 603 603 _readInlineState: function(state) { 604 604 this._id = state.id; 605 + this._originalState = state.contentState; 606 + this._canSuggestEdit = state.canSuggestEdit; 605 607 }, 606 608 607 609 _ondeleteresponse: function() { ··· 664 666 _drawEditRows: function(rows) { 665 667 this.setEditing(true); 666 668 this._editRow = this._drawRows(rows, null, 'edit'); 669 + 670 + this._drawSuggestionState(this._editRow); 671 + JX.log(this._originalState); 672 + 673 + this.setHasSuggestion(this._originalState.hasSuggestion); 667 674 }, 668 675 669 676 _drawRows: function(rows, cursor, type) { ··· 719 726 return result_row; 720 727 }, 721 728 729 + _drawSuggestionState: function(row) { 730 + if (this._canSuggestEdit) { 731 + var button = this._getSuggestionButton(); 732 + var node = button.getNode(); 733 + 734 + // As a side effect of form submission, the button may become 735 + // visually disabled. Re-enable it. This is a bit hacky. 736 + JX.DOM.alterClass(node, 'disabled', false); 737 + node.disabled = false; 738 + 739 + var container = JX.DOM.find(row, 'div', 'inline-edit-buttons'); 740 + container.appendChild(node); 741 + } 742 + }, 743 + 744 + _getSuggestionButton: function() { 745 + if (!this._suggestionButton) { 746 + var button = new JX.PHUIXButtonView() 747 + .setIcon('fa-pencil-square-o') 748 + .setColor('grey'); 749 + 750 + var node = button.getNode(); 751 + JX.DOM.alterClass(node, 'inline-button-left', true); 752 + 753 + var onclick = JX.bind(this, this._onSuggestEdit); 754 + JX.DOM.listen(node, 'click', null, onclick); 755 + 756 + this._suggestionButton = button; 757 + } 758 + 759 + return this._suggestionButton; 760 + }, 761 + 762 + _onSuggestEdit: function(e) { 763 + e.kill(); 764 + 765 + this.setHasSuggestion(!this.getHasSuggestion()); 766 + 767 + // The first time the user actually clicks the button and enables 768 + // suggestions for a given editor state, fill the input with the 769 + // underlying text if there isn't any text yet. 770 + if (this.getHasSuggestion()) { 771 + if (this._editRow) { 772 + var node = this._getSuggestionNode(this._editRow); 773 + if (node) { 774 + if (!node.value.length) { 775 + var data = JX.Stratcom.getData(node); 776 + if (!data.hasSetDefault) { 777 + data.hasSetDefault = true; 778 + node.value = data.defaultText; 779 + node.rows = Math.max(3, node.value.split('\n').length); 780 + } 781 + } 782 + } 783 + } 784 + } 785 + 786 + // Save the "hasSuggestion" part of the content state. 787 + this.triggerDraft(); 788 + }, 789 + 790 + setHasSuggestion: function(has_suggestion) { 791 + this._hasSuggestion = has_suggestion; 792 + 793 + var button = this._getSuggestionButton(); 794 + var pht = this.getChangeset().getChangesetList().getTranslations(); 795 + if (has_suggestion) { 796 + button 797 + .setIcon('fa-times') 798 + .setText(pht('Discard Edit')); 799 + } else { 800 + button 801 + .setIcon('fa-plus') 802 + .setText(pht('Suggest Edit')); 803 + } 804 + 805 + if (this._editRow) { 806 + JX.DOM.alterClass(this._editRow, 'has-suggestion', has_suggestion); 807 + } 808 + }, 809 + 810 + getHasSuggestion: function() { 811 + return this._hasSuggestion; 812 + }, 813 + 722 814 save: function() { 723 815 var handler = JX.bind(this, this._onsubmitresponse); 724 816 ··· 825 917 // Ignore. 826 918 } 827 919 828 - try { 829 - node = JX.DOM.find(row, 'textarea', 'inline-content-suggestion'); 920 + node = this._getSuggestionNode(row); 921 + if (node) { 830 922 state.suggestionText = node.value; 831 - } catch (ex) { 832 - // Ignore. 833 923 } 924 + 925 + state.hasSuggestion = this.getHasSuggestion(); 834 926 835 927 return state; 928 + }, 929 + 930 + _getSuggestionNode: function(row) { 931 + try { 932 + return JX.DOM.find(row, 'textarea', 'inline-content-suggestion'); 933 + } catch (ex) { 934 + return null; 935 + } 836 936 }, 837 937 838 938 _onsubmitresponse: function(response) { ··· 1063 1163 return { 1064 1164 text: '', 1065 1165 suggestionText: '', 1066 - hasSuggestion: true 1166 + hasSuggestion: false 1067 1167 }; 1068 1168 }, 1069 1169 ··· 1073 1173 1074 1174 _isSameContentState: function(u, v) { 1075 1175 return ( 1176 + ((u === null) === (v === null)) && 1076 1177 (u.text === v.text) && 1077 1178 (u.suggestionText === v.suggestionText) && 1078 1179 (u.hasSuggestion === v.hasSuggestion));