@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.

Render indent depth changes more clearly

Summary:
Ref T13161. See PHI723. Our whitespace handling is based on whitespace flags like `diff -bw`, mostly just for historical reasons: long ago, the easiest way to minimize the visual impact of indentation changes was to literally use `diff -bw`.

However, this approach is very coarse and has a lot of problems, like detecting `"ab" -> "a b"` as "only a whitespace change" even though this is always semantic. It also causes problems in YAML, Python, etc. Over time, we've added a lot of stuff to mitigate the downsides to this approach.

We also no longer get any benefits from this approach being simple: we need faithful diffs as the authoritative source, and have to completely rebuild the diff to `diff -bw` it. In the UI, we have a "whitespace mode" flag. We have the "whitespace matters" configuration.

I think ReviewBoard generally has a better approach to indent depth changes than we do (see T13161) where it detects them and renders them in a minimal way with low visual impact. This is ultimately what we want: reduce visual clutter for depth-only changes, but preserve whitespace changes in strings, etc.

Move toward detecting and rendering indent depth changes. Followup work:

- These should get colorblind colors and the design can probably use a little more tweaking.
- The OneUp mode is okay, but could be improved.
- Whitespace mode can now be removed completely.
- I'm trying to handle tabs correctly, but since we currently mangle them into spaces today, it's hard to be sure I actually got it right.

Test Plan: {F6214084}

Reviewers: amckinley

Reviewed By: amckinley

Maniphest Tasks: T13161

Differential Revision: https://secure.phabricator.com/D20181

+251 -16
+8 -6
resources/celerity/map.php
··· 11 11 'conpherence.pkg.js' => '020aebcf', 12 12 'core.pkg.css' => '261ee8cf', 13 13 'core.pkg.js' => '5ace8a1e', 14 - 'differential.pkg.css' => 'b8df73d4', 14 + 'differential.pkg.css' => 'c3f15714', 15 15 'differential.pkg.js' => '67c9ea4c', 16 16 'diffusion.pkg.css' => '42c75c37', 17 17 'diffusion.pkg.js' => '91192d85', ··· 61 61 'rsrc/css/application/dashboard/dashboard.css' => '4267d6c6', 62 62 'rsrc/css/application/diff/inline-comment-summary.css' => '81eb368d', 63 63 'rsrc/css/application/differential/add-comment.css' => '7e5900d9', 64 - 'rsrc/css/application/differential/changeset-view.css' => '73660575', 64 + 'rsrc/css/application/differential/changeset-view.css' => '783a9206', 65 65 'rsrc/css/application/differential/core.css' => 'bdb93065', 66 66 'rsrc/css/application/differential/phui-inline-comment.css' => '48acce5b', 67 67 'rsrc/css/application/differential/revision-comment.css' => '7dbc8d1d', ··· 275 275 'rsrc/image/checker_dark.png' => '7fc8fa7b', 276 276 'rsrc/image/checker_light.png' => '3157a202', 277 277 'rsrc/image/checker_lighter.png' => 'c45928c1', 278 + 'rsrc/image/chevron-in.png' => '1aa2f88f', 279 + 'rsrc/image/chevron-out.png' => 'c815e272', 278 280 'rsrc/image/controls/checkbox-checked.png' => '1770d7a0', 279 281 'rsrc/image/controls/checkbox-unchecked.png' => 'e1deba0a', 280 282 'rsrc/image/d5d8e1.png' => '6764616e', ··· 539 541 'conpherence-thread-manager' => 'aec8e38c', 540 542 'conpherence-transaction-css' => '3a3f5e7e', 541 543 'd3' => 'd67475f5', 542 - 'differential-changeset-view-css' => '73660575', 544 + 'differential-changeset-view-css' => '783a9206', 543 545 'differential-core-view-css' => 'bdb93065', 544 546 'differential-revision-add-comment-css' => '7e5900d9', 545 547 'differential-revision-comment-css' => '7dbc8d1d', ··· 1490 1492 'javelin-dom', 1491 1493 'javelin-uri', 1492 1494 ), 1493 - 73660575 => array( 1494 - 'phui-inline-comment-view-css', 1495 - ), 1496 1495 '73ecc1f8' => array( 1497 1496 'javelin-behavior', 1498 1497 'javelin-behavior-device', ··· 1513 1512 'javelin-dom', 1514 1513 'javelin-uri', 1515 1514 'javelin-request', 1515 + ), 1516 + '783a9206' => array( 1517 + 'phui-inline-comment-view-css', 1516 1518 ), 1517 1519 '78bc5d94' => array( 1518 1520 'javelin-behavior',
+2
src/applications/celerity/postprocessor/CelerityDefaultPostprocessor.php
··· 199 199 'diff.background' => '#fff', 200 200 'new-background' => 'rgba(151, 234, 151, .3)', 201 201 'new-bright' => 'rgba(151, 234, 151, .6)', 202 + 'new-background-strong' => 'rgba(151, 234, 151, 1)', 202 203 'old-background' => 'rgba(251, 175, 175, .3)', 203 204 'old-bright' => 'rgba(251, 175, 175, .7)', 205 + 'old-background-strong' => 'rgba(251, 175, 175, 1)', 204 206 'move-background' => '#fdf5d4', 205 207 'copy-background' => '#f1c40f', 206 208
+1 -1
src/applications/differential/__tests__/data/whitespace.diff.one.whitespace
··· 1 1 O 1 - -=[-Rocket-Ship>\n~ 2 - N 1 + {( )}-=[-Rocket-Ship>\n~ 2 + N 1 + {> )}-=[-Rocket-Ship>\n~
+1 -1
src/applications/differential/__tests__/data/whitespace.diff.two.whitespace
··· 1 1 O 1 - -=[-Rocket-Ship>\n~ 2 - N 1 + {( )}-=[-Rocket-Ship>\n~ 2 + N 1 + {> )}-=[-Rocket-Ship>\n~
+15 -2
src/applications/differential/parser/DifferentialChangesetParser.php
··· 8 8 protected $new = array(); 9 9 protected $old = array(); 10 10 protected $intra = array(); 11 + protected $depthOnlyLines = array(); 11 12 protected $newRender = null; 12 13 protected $oldRender = null; 13 14 ··· 190 191 return $this; 191 192 } 192 193 193 - const CACHE_VERSION = 11; 194 + const CACHE_VERSION = 12; 194 195 const CACHE_MAX_SIZE = 8e6; 195 196 196 197 const ATTR_GENERATED = 'attr:generated'; ··· 224 225 return $this; 225 226 } 226 227 228 + public function setDepthOnlyLines(array $lines) { 229 + $this->depthOnlyLines = $lines; 230 + return $this; 231 + } 232 + 233 + public function getDepthOnlyLines() { 234 + return $this->depthOnlyLines; 235 + } 236 + 227 237 public function setVisibileLinesMask(array $mask) { 228 238 $this->visible = $mask; 229 239 return $this; ··· 450 460 'new', 451 461 'old', 452 462 'intra', 463 + 'depthOnlyLines', 453 464 'newRender', 454 465 'oldRender', 455 466 'specialAttributes', ··· 754 765 $this->setOldLines($hunk_parser->getOldLines()); 755 766 $this->setNewLines($hunk_parser->getNewLines()); 756 767 $this->setIntraLineDiffs($hunk_parser->getIntraLineDiffs()); 768 + $this->setDepthOnlyLines($hunk_parser->getDepthOnlyLines()); 757 769 $this->setVisibileLinesMask($hunk_parser->getVisibleLinesMask()); 758 770 $this->hunkStartLines = $hunk_parser->getHunkStartLines( 759 771 $changeset->getHunks()); ··· 914 926 ->setShowEditAndReplyLinks($this->getShowEditAndReplyLinks()) 915 927 ->setCanMarkDone($this->getCanMarkDone()) 916 928 ->setObjectOwnerPHID($this->getObjectOwnerPHID()) 917 - ->setHighlightingDisabled($this->highlightingDisabled); 929 + ->setHighlightingDisabled($this->highlightingDisabled) 930 + ->setDepthOnlyLines($this->getDepthOnlyLines()); 918 931 919 932 $shield = null; 920 933 if ($this->isTopLevel && !$this->comments) {
+168 -3
src/applications/differential/parser/DifferentialHunkParser.php
··· 5 5 private $oldLines; 6 6 private $newLines; 7 7 private $intraLineDiffs; 8 + private $depthOnlyLines; 8 9 private $visibleLinesMask; 9 10 private $whitespaceMode; 10 11 ··· 115 116 return $this; 116 117 } 117 118 119 + public function setDepthOnlyLines(array $map) { 120 + $this->depthOnlyLines = $map; 121 + return $this; 122 + } 123 + 124 + public function getDepthOnlyLines() { 125 + return $this->depthOnlyLines; 126 + } 118 127 119 128 public function setWhitespaceMode($white_space_mode) { 120 129 $this->whitespaceMode = $white_space_mode; ··· 334 343 $new = $this->getNewLines(); 335 344 336 345 $diffs = array(); 346 + $depth_only = array(); 337 347 foreach ($old as $key => $o) { 338 348 $n = $new[$key]; 339 349 ··· 342 352 } 343 353 344 354 if ($o['type'] != $n['type']) { 345 - $diffs[$key] = ArcanistDiffUtils::generateIntralineDiff( 346 - $o['text'], 347 - $n['text']); 355 + $o_segments = array(); 356 + $n_segments = array(); 357 + $tab_width = 2; 358 + 359 + $o_text = $o['text']; 360 + $n_text = $n['text']; 361 + 362 + if ($o_text !== $n_text) { 363 + $o_depth = $this->getIndentDepth($o_text, $tab_width); 364 + $n_depth = $this->getIndentDepth($n_text, $tab_width); 365 + 366 + if ($o_depth < $n_depth) { 367 + $segment_type = '>'; 368 + $segment_width = $this->getCharacterCountForVisualWhitespace( 369 + $n_text, 370 + ($n_depth - $o_depth), 371 + $tab_width); 372 + if ($segment_width) { 373 + $n_text = substr($n_text, $segment_width); 374 + $n_segments[] = array( 375 + $segment_type, 376 + $segment_width, 377 + ); 378 + } 379 + } else if ($o_depth > $n_depth) { 380 + $segment_type = '<'; 381 + $segment_width = $this->getCharacterCountForVisualWhitespace( 382 + $o_text, 383 + ($o_depth - $n_depth), 384 + $tab_width); 385 + if ($segment_width) { 386 + $o_text = substr($o_text, $segment_width); 387 + $o_segments[] = array( 388 + $segment_type, 389 + $segment_width, 390 + ); 391 + } 392 + } 393 + 394 + // If there are no remaining changes to this line after we've marked 395 + // off the indent depth changes, this line was only modified by 396 + // changing the indent depth. Mark it for later so we can change how 397 + // it is displayed. 398 + if ($o_text === $n_text) { 399 + $depth_only[$key] = $segment_type; 400 + } 401 + } 402 + 403 + $intraline_segments = ArcanistDiffUtils::generateIntralineDiff( 404 + $o_text, 405 + $n_text); 406 + 407 + foreach ($intraline_segments[0] as $o_segment) { 408 + $o_segments[] = $o_segment; 409 + } 410 + 411 + foreach ($intraline_segments[1] as $n_segment) { 412 + $n_segments[] = $n_segment; 413 + } 414 + 415 + $diffs[$key] = array( 416 + $o_segments, 417 + $n_segments, 418 + ); 348 419 } 349 420 } 350 421 351 422 $this->setIntraLineDiffs($diffs); 423 + $this->setDepthOnlyLines($depth_only); 352 424 353 425 return $this; 354 426 } ··· 671 743 672 744 return $offsets; 673 745 } 746 + 747 + private function getIndentDepth($text, $tab_width) { 748 + $len = strlen($text); 749 + 750 + $depth = 0; 751 + for ($ii = 0; $ii < $len; $ii++) { 752 + $c = $text[$ii]; 753 + 754 + // If this is a space, increase the indent depth by 1. 755 + if ($c == ' ') { 756 + $depth++; 757 + continue; 758 + } 759 + 760 + // If this is a tab, increase the indent depth to the next tabstop. 761 + 762 + // For example, if the tab width is 4, these sequences both lead us to 763 + // a visual width of 8, i.e. the cursor will be in the 8th column: 764 + // 765 + // <tab><tab> 766 + // <space><tab><space><space><space><tab> 767 + 768 + if ($c == "\t") { 769 + $depth = ($depth + $tab_width); 770 + $depth = $depth - ($depth % $tab_width); 771 + continue; 772 + } 773 + 774 + break; 775 + } 776 + 777 + return $depth; 778 + } 779 + 780 + private function getCharacterCountForVisualWhitespace( 781 + $text, 782 + $depth, 783 + $tab_width) { 784 + 785 + // Here, we know the visual indent depth of a line has been increased by 786 + // some amount (for example, 6 characters). 787 + 788 + // We want to find the largest whitespace prefix of the string we can 789 + // which still fits into that amount of visual space. 790 + 791 + // In most cases, this is very easy. For example, if the string has been 792 + // indented by two characters and the string begins with two spaces, that's 793 + // a perfect match. 794 + 795 + // However, if the string has been indented by 7 characters, the tab width 796 + // is 8, and the string begins with "<space><space><tab>", we can only 797 + // mark the two spaces as an indent change. These cases are unusual. 798 + 799 + $character_depth = 0; 800 + $visual_depth = 0; 801 + 802 + $len = strlen($text); 803 + for ($ii = 0; $ii < $len; $ii++) { 804 + if ($visual_depth >= $depth) { 805 + break; 806 + } 807 + 808 + $c = $text[$ii]; 809 + 810 + if ($c == ' ') { 811 + $character_depth++; 812 + $visual_depth++; 813 + continue; 814 + } 815 + 816 + if ($c == "\t") { 817 + // Figure out how many visual spaces we have until the next tabstop. 818 + $tab_visual = ($visual_depth + $tab_width); 819 + $tab_visual = $tab_visual - ($tab_visual % $tab_width); 820 + $tab_visual = ($tab_visual - $visual_depth); 821 + 822 + // If this tab would take us over the limit, we're all done. 823 + $remaining_depth = ($depth - $visual_depth); 824 + if ($remaining_depth < $tab_visual) { 825 + break; 826 + } 827 + 828 + $character_depth++; 829 + $visual_depth += $tab_visual; 830 + continue; 831 + } 832 + 833 + break; 834 + } 835 + 836 + return $character_depth; 837 + } 838 + 674 839 }
+10
src/applications/differential/render/DifferentialChangesetRenderer.php
··· 34 34 private $objectOwnerPHID; 35 35 private $highlightingDisabled; 36 36 private $scopeEngine; 37 + private $depthOnlyLines; 37 38 38 39 private $oldFile = false; 39 40 private $newFile = false; ··· 90 91 } 91 92 protected function getGaps() { 92 93 return $this->gaps; 94 + } 95 + 96 + public function setDepthOnlyLines(array $lines) { 97 + $this->depthOnlyLines = $lines; 98 + return $this; 99 + } 100 + 101 + public function getDepthOnlyLines() { 102 + return $this->depthOnlyLines; 93 103 } 94 104 95 105 public function attachOldFile(PhabricatorFile $old = null) {
+4
src/applications/differential/render/DifferentialChangesetTestRenderer.php
··· 96 96 array( 97 97 '<span class="bright">', 98 98 '</span>', 99 + '<span class="depth-out">', 100 + '<span class="depth-in">', 99 101 ), 100 102 array( 101 103 '{(', 102 104 ')}', 105 + '{<', 106 + '{>', 103 107 ), 104 108 $render); 105 109
+21 -3
src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php
··· 71 71 $mask = $this->getMask(); 72 72 73 73 $scope_engine = $this->getScopeEngine(); 74 - 75 74 $offset_map = null; 75 + $depth_only = $this->getDepthOnlyLines(); 76 76 77 77 for ($ii = $range_start; $ii < $range_start + $range_len; $ii++) { 78 78 if (empty($mask[$ii])) { ··· 196 196 } else if (empty($old_lines[$ii])) { 197 197 $n_class = 'new new-full'; 198 198 } else { 199 - $n_class = 'new'; 199 + 200 + // NOTE: At least for the moment, I'm intentionally clearing the 201 + // line highlighting only on the right side of the diff when a 202 + // line has only depth changes. When a block depth is decreased, 203 + // this gives us a large color block on the left (to make it easy 204 + // to see the depth change) but a clean diff on the right (to make 205 + // it easy to pick out actual code changes). 206 + 207 + if (isset($depth_only[$ii])) { 208 + $n_class = ''; 209 + } else { 210 + $n_class = 'new'; 211 + } 200 212 } 201 213 $n_classes = $n_class; 202 214 203 - if ($new_lines[$ii]['type'] == '\\' || !isset($copy_lines[$n_num])) { 215 + $not_copied = 216 + // If this line only changed depth, copy markers are pointless. 217 + (!isset($copy_lines[$n_num])) || 218 + (isset($depth_only[$ii])) || 219 + ($new_lines[$ii]['type'] == '\\'); 220 + 221 + if ($not_copied) { 204 222 $n_copy = phutil_tag('td', array('class' => "copy {$n_class}")); 205 223 } else { 206 224 list($orig_file, $orig_line, $orig_type) = $copy_lines[$n_num];
+21
webroot/rsrc/css/application/differential/changeset-view.css
··· 135 135 background: {$old-bright}; 136 136 } 137 137 138 + 138 139 .differential-diff td.new span.bright, 139 140 .differential-diff td.new-full, 140 141 .prose-diff span.new { 141 142 background: {$new-bright}; 142 143 } 144 + 145 + .differential-diff td span.depth-out, 146 + .differential-diff td span.depth-in { 147 + padding: 2px 0; 148 + background-size: 12px 12px; 149 + background-repeat: no-repeat; 150 + background-position: left center; 151 + } 152 + 153 + .differential-diff td span.depth-out { 154 + background-image: url(/rsrc/image/chevron-out.png); 155 + background-color: {$old-background-strong}; 156 + } 157 + 158 + .differential-diff td span.depth-in { 159 + background-position: 2px center; 160 + background-image: url(/rsrc/image/chevron-in.png); 161 + background-color: {$new-background-strong}; 162 + } 163 + 143 164 144 165 .differential-diff td.copy { 145 166 min-width: 0.5%;
webroot/rsrc/image/chevron-in.png

This is a binary file and will not be displayed.

webroot/rsrc/image/chevron-out.png

This is a binary file and will not be displayed.