Correct Aphlict websocket URI construction after PHP8 compatibility changes
[phabricator.git] / src / applications / differential / parser / DifferentialHunkParser.php
blob2902022184294d3e2b4ecf3a1242d9b6caa0c71a
1 <?php
3 final class DifferentialHunkParser extends Phobject {
5 private $oldLines;
6 private $newLines;
7 private $intraLineDiffs;
8 private $depthOnlyLines;
9 private $visibleLinesMask;
10 private $normalized;
12 /**
13 * Get a map of lines on which hunks start, other than line 1. This
14 * datastructure is used to determine when to render "Context not available."
15 * in diffs with multiple hunks.
17 * @return dict<int, bool> Map of lines where hunks start, other than line 1.
19 public function getHunkStartLines(array $hunks) {
20 assert_instances_of($hunks, 'DifferentialHunk');
22 $map = array();
23 foreach ($hunks as $hunk) {
24 $line = $hunk->getOldOffset();
25 if ($line > 1) {
26 $map[$line] = true;
30 return $map;
33 private function setVisibleLinesMask($mask) {
34 $this->visibleLinesMask = $mask;
35 return $this;
37 public function getVisibleLinesMask() {
38 if ($this->visibleLinesMask === null) {
39 throw new PhutilInvalidStateException('generateVisibleLinesMask');
41 return $this->visibleLinesMask;
44 private function setIntraLineDiffs($intra_line_diffs) {
45 $this->intraLineDiffs = $intra_line_diffs;
46 return $this;
48 public function getIntraLineDiffs() {
49 if ($this->intraLineDiffs === null) {
50 throw new PhutilInvalidStateException('generateIntraLineDiffs');
52 return $this->intraLineDiffs;
55 private function setNewLines($new_lines) {
56 $this->newLines = $new_lines;
57 return $this;
59 public function getNewLines() {
60 if ($this->newLines === null) {
61 throw new PhutilInvalidStateException('parseHunksForLineData');
63 return $this->newLines;
66 private function setOldLines($old_lines) {
67 $this->oldLines = $old_lines;
68 return $this;
70 public function getOldLines() {
71 if ($this->oldLines === null) {
72 throw new PhutilInvalidStateException('parseHunksForLineData');
74 return $this->oldLines;
77 public function getOldLineTypeMap() {
78 $map = array();
79 $old = $this->getOldLines();
80 foreach ($old as $o) {
81 if (!$o) {
82 continue;
84 $map[$o['line']] = $o['type'];
86 return $map;
89 public function setOldLineTypeMap(array $map) {
90 $lines = $this->getOldLines();
91 foreach ($lines as $key => $data) {
92 $lines[$key]['type'] = idx($map, $data['line']);
94 $this->oldLines = $lines;
95 return $this;
98 public function getNewLineTypeMap() {
99 $map = array();
100 $new = $this->getNewLines();
101 foreach ($new as $n) {
102 if (!$n) {
103 continue;
105 $map[$n['line']] = $n['type'];
107 return $map;
110 public function setNewLineTypeMap(array $map) {
111 $lines = $this->getNewLines();
112 foreach ($lines as $key => $data) {
113 $lines[$key]['type'] = idx($map, $data['line']);
115 $this->newLines = $lines;
116 return $this;
119 public function setDepthOnlyLines(array $map) {
120 $this->depthOnlyLines = $map;
121 return $this;
124 public function getDepthOnlyLines() {
125 return $this->depthOnlyLines;
128 public function setNormalized($normalized) {
129 $this->normalized = $normalized;
130 return $this;
133 public function getNormalized() {
134 return $this->normalized;
137 public function getIsDeleted() {
138 foreach ($this->getNewLines() as $line) {
139 if ($line) {
140 // At least one new line, so the entire file wasn't deleted.
141 return false;
145 foreach ($this->getOldLines() as $line) {
146 if ($line) {
147 // No new lines, at least one old line; the entire file was deleted.
148 return true;
152 // This is an empty file.
153 return false;
157 * Returns true if the hunks change anything, including whitespace.
159 public function getHasAnyChanges() {
160 return $this->getHasChanges('any');
163 private function getHasChanges($filter) {
164 if ($filter !== 'any' && $filter !== 'text') {
165 throw new Exception(pht("Unknown change filter '%s'.", $filter));
168 $old = $this->getOldLines();
169 $new = $this->getNewLines();
171 $is_any = ($filter === 'any');
173 foreach ($old as $key => $o) {
174 $n = $new[$key];
175 if ($o === null || $n === null) {
176 // One side is missing, and it's impossible for both sides to be null,
177 // so the other side must have something, and thus the two sides are
178 // different and the file has been changed under any type of filter.
179 return true;
182 if ($o['type'] !== $n['type']) {
183 return true;
186 if ($o['text'] !== $n['text']) {
187 if ($is_any) {
188 // The text is different, so there's a change.
189 return true;
190 } else if (trim($o['text']) !== trim($n['text'])) {
191 return true;
196 // No changes anywhere in the file.
197 return false;
202 * This function takes advantage of the parsing work done in
203 * @{method:parseHunksForLineData} and continues the struggle to hammer this
204 * data into something we can display to a user.
206 * In particular, this function re-parses the hunks to make them equivalent
207 * in length for easy rendering, adding `null` as necessary to pad the
208 * length.
210 * Anyhoo, this function is not particularly well-named but I try.
212 * NOTE: this function must be called after
213 * @{method:parseHunksForLineData}.
215 public function reparseHunksForSpecialAttributes() {
216 $rebuild_old = array();
217 $rebuild_new = array();
219 $old_lines = array_reverse($this->getOldLines());
220 $new_lines = array_reverse($this->getNewLines());
222 while (count($old_lines) || count($new_lines)) {
223 $old_line_data = array_pop($old_lines);
224 $new_line_data = array_pop($new_lines);
226 if ($old_line_data) {
227 $o_type = $old_line_data['type'];
228 } else {
229 $o_type = null;
232 if ($new_line_data) {
233 $n_type = $new_line_data['type'];
234 } else {
235 $n_type = null;
238 // This line does not exist in the new file.
239 if (($o_type != null) && ($n_type == null)) {
240 $rebuild_old[] = $old_line_data;
241 $rebuild_new[] = null;
242 if ($new_line_data) {
243 array_push($new_lines, $new_line_data);
245 continue;
248 // This line does not exist in the old file.
249 if (($n_type != null) && ($o_type == null)) {
250 $rebuild_old[] = null;
251 $rebuild_new[] = $new_line_data;
252 if ($old_line_data) {
253 array_push($old_lines, $old_line_data);
255 continue;
258 $rebuild_old[] = $old_line_data;
259 $rebuild_new[] = $new_line_data;
262 $this->setOldLines($rebuild_old);
263 $this->setNewLines($rebuild_new);
265 $this->updateChangeTypesForNormalization();
267 return $this;
270 public function generateIntraLineDiffs() {
271 $old = $this->getOldLines();
272 $new = $this->getNewLines();
274 $diffs = array();
275 $depth_only = array();
276 foreach ($old as $key => $o) {
277 $n = $new[$key];
279 if (!$o || !$n) {
280 continue;
283 if ($o['type'] != $n['type']) {
284 $o_segments = array();
285 $n_segments = array();
286 $tab_width = 2;
288 $o_text = $o['text'];
289 $n_text = $n['text'];
291 if ($o_text !== $n_text && (ltrim($o_text) === ltrim($n_text))) {
292 $o_depth = $this->getIndentDepth($o_text, $tab_width);
293 $n_depth = $this->getIndentDepth($n_text, $tab_width);
295 if ($o_depth < $n_depth) {
296 $segment_type = '>';
297 $segment_width = $this->getCharacterCountForVisualWhitespace(
298 $n_text,
299 ($n_depth - $o_depth),
300 $tab_width);
301 if ($segment_width) {
302 $n_text = substr($n_text, $segment_width);
303 $n_segments[] = array(
304 $segment_type,
305 $segment_width,
308 } else if ($o_depth > $n_depth) {
309 $segment_type = '<';
310 $segment_width = $this->getCharacterCountForVisualWhitespace(
311 $o_text,
312 ($o_depth - $n_depth),
313 $tab_width);
314 if ($segment_width) {
315 $o_text = substr($o_text, $segment_width);
316 $o_segments[] = array(
317 $segment_type,
318 $segment_width,
323 // If there are no remaining changes to this line after we've marked
324 // off the indent depth changes, this line was only modified by
325 // changing the indent depth. Mark it for later so we can change how
326 // it is displayed.
327 if ($o_text === $n_text) {
328 $depth_only[$key] = $segment_type;
332 $intraline_segments = ArcanistDiffUtils::generateIntralineDiff(
333 $o_text,
334 $n_text);
336 foreach ($intraline_segments[0] as $o_segment) {
337 $o_segments[] = $o_segment;
340 foreach ($intraline_segments[1] as $n_segment) {
341 $n_segments[] = $n_segment;
344 $diffs[$key] = array(
345 $o_segments,
346 $n_segments,
351 $this->setIntraLineDiffs($diffs);
352 $this->setDepthOnlyLines($depth_only);
354 return $this;
357 public function generateVisibleBlocksMask($lines_context) {
359 // See T13468. This is similar to "generateVisibleLinesMask()", but
360 // attempts to work around a series of bugs which cancel each other
361 // out but make a mess of the intermediate steps.
363 $old = $this->getOldLines();
364 $new = $this->getNewLines();
366 $length = max(count($old), count($new));
368 $visible_lines = array();
369 for ($ii = 0; $ii < $length; $ii++) {
370 $old_visible = (isset($old[$ii]) && $old[$ii]['type']);
371 $new_visible = (isset($new[$ii]) && $new[$ii]['type']);
373 $visible_lines[$ii] = ($old_visible || $new_visible);
376 $mask = array();
377 $reveal_cursor = -1;
378 for ($ii = 0; $ii < $length; $ii++) {
380 // If this line isn't visible, it isn't going to reveal anything.
381 if (!$visible_lines[$ii]) {
383 // If it hasn't been revealed by a nearby line, mark it as masked.
384 if (empty($mask[$ii])) {
385 $mask[$ii] = false;
388 continue;
391 // If this line is visible, reveal all the lines nearby.
393 // First, compute the minimum and maximum offsets we want to reveal.
394 $min_reveal = max($ii - $lines_context, 0);
395 $max_reveal = min($ii + $lines_context, $length - 1);
397 // Naively, we'd do more work than necessary when revealing context for
398 // several adjacent visible lines: we would mark all the overlapping
399 // lines as revealed several times.
401 // To avoid duplicating work, keep track of the largest line we've
402 // revealed to. Since we reveal context by marking every consecutive
403 // line, we don't need to touch any line above it.
404 $min_reveal = max($min_reveal, $reveal_cursor);
406 // Reveal the remaining unrevealed lines.
407 for ($jj = $min_reveal; $jj <= $max_reveal; $jj++) {
408 $mask[$jj] = true;
411 // Move the cursor to the next line which may still need to be revealed.
412 $reveal_cursor = $max_reveal + 1;
415 $this->setVisibleLinesMask($mask);
417 return $mask;
420 public function generateVisibleLinesMask($lines_context) {
421 $old = $this->getOldLines();
422 $new = $this->getNewLines();
423 $max_length = max(count($old), count($new));
424 $visible = false;
425 $last = 0;
426 $mask = array();
428 for ($cursor = -$lines_context; $cursor < $max_length; $cursor++) {
429 $offset = $cursor + $lines_context;
430 if ((isset($old[$offset]) && $old[$offset]['type']) ||
431 (isset($new[$offset]) && $new[$offset]['type'])) {
432 $visible = true;
433 $last = $offset;
434 } else if ($cursor > $last + $lines_context) {
435 $visible = false;
437 if ($visible && $cursor > 0) {
438 $mask[$cursor] = 1;
442 $this->setVisibleLinesMask($mask);
444 return $this;
447 public function getOldCorpus() {
448 return $this->getCorpus($this->getOldLines());
451 public function getNewCorpus() {
452 return $this->getCorpus($this->getNewLines());
455 private function getCorpus(array $lines) {
457 $corpus = array();
458 foreach ($lines as $l) {
459 if ($l === null) {
460 $corpus[] = "\n";
461 continue;
464 if ($l['type'] != '\\') {
465 if ($l['text'] === null) {
466 // There's no text on this side of the diff, but insert a placeholder
467 // newline so the highlighted line numbers match up.
468 $corpus[] = "\n";
469 } else {
470 $corpus[] = $l['text'];
474 return $corpus;
477 public function parseHunksForLineData(array $hunks) {
478 assert_instances_of($hunks, 'DifferentialHunk');
480 $old_lines = array();
481 $new_lines = array();
482 foreach ($hunks as $hunk) {
483 $lines = $hunk->getSplitLines();
485 $line_type_map = array();
486 $line_text = array();
487 foreach ($lines as $line_index => $line) {
488 if (isset($line[0])) {
489 $char = $line[0];
490 switch ($char) {
491 case ' ':
492 $line_type_map[$line_index] = null;
493 $line_text[$line_index] = substr($line, 1);
494 break;
495 case "\r":
496 case "\n":
497 // NOTE: Normally, the first character is a space, plus, minus or
498 // backslash, but it may be a newline if it used to be a space and
499 // trailing whitespace has been stripped via email transmission or
500 // some similar mechanism. In these cases, we essentially pretend
501 // the missing space is still there.
502 $line_type_map[$line_index] = null;
503 $line_text[$line_index] = $line;
504 break;
505 case '+':
506 case '-':
507 case '\\':
508 $line_type_map[$line_index] = $char;
509 $line_text[$line_index] = substr($line, 1);
510 break;
511 default:
512 throw new Exception(
513 pht(
514 'Unexpected leading character "%s" at line index %s!',
515 $char,
516 $line_index));
518 } else {
519 $line_type_map[$line_index] = null;
520 $line_text[$line_index] = '';
524 $old_line = $hunk->getOldOffset();
525 $new_line = $hunk->getNewOffset();
527 $num_lines = count($lines);
528 for ($cursor = 0; $cursor < $num_lines; $cursor++) {
529 $type = $line_type_map[$cursor];
530 $data = array(
531 'type' => $type,
532 'text' => $line_text[$cursor],
533 'line' => $new_line,
535 if ($type == '\\') {
536 $type = $line_type_map[$cursor - 1];
537 $data['text'] = ltrim($data['text']);
539 switch ($type) {
540 case '+':
541 $new_lines[] = $data;
542 ++$new_line;
543 break;
544 case '-':
545 $data['line'] = $old_line;
546 $old_lines[] = $data;
547 ++$old_line;
548 break;
549 default:
550 $new_lines[] = $data;
551 $data['line'] = $old_line;
552 $old_lines[] = $data;
553 ++$new_line;
554 ++$old_line;
555 break;
560 $this->setOldLines($old_lines);
561 $this->setNewLines($new_lines);
563 return $this;
566 public function parseHunksForHighlightMasks(
567 array $changeset_hunks,
568 array $old_hunks,
569 array $new_hunks) {
570 assert_instances_of($changeset_hunks, 'DifferentialHunk');
571 assert_instances_of($old_hunks, 'DifferentialHunk');
572 assert_instances_of($new_hunks, 'DifferentialHunk');
574 // Put changes side by side.
575 $olds = array();
576 $news = array();
577 $olds_cursor = -1;
578 $news_cursor = -1;
579 foreach ($changeset_hunks as $hunk) {
580 $n_old = $hunk->getOldOffset();
581 $n_new = $hunk->getNewOffset();
582 $changes = $hunk->getSplitLines();
583 foreach ($changes as $line) {
584 $diff_type = $line[0]; // Change type in diff of diffs.
585 $is_same = ($diff_type === ' ');
586 $is_add = ($diff_type === '+');
587 $is_rem = ($diff_type === '-');
589 $orig_type = $line[1]; // Change type in the original diff.
591 if ($is_same) {
592 // Use the same key for lines that are next to each other.
593 if ($olds_cursor > $news_cursor) {
594 $key = $olds_cursor + 1;
595 } else {
596 $key = $news_cursor + 1;
598 $olds[$key] = null;
599 $news[$key] = null;
600 $olds_cursor = $key;
601 $news_cursor = $key;
602 } else if ($is_rem) {
603 $olds[] = array($n_old, $orig_type);
604 $olds_cursor++;
605 } else if ($is_add) {
606 $news[] = array($n_new, $orig_type);
607 $news_cursor++;
608 } else {
609 throw new Exception(
610 pht(
611 'Found unknown intradiff source line, expected a line '.
612 'beginning with "+", "-", or " " (space): %s.',
613 $line));
616 // See T13539. Don't increment the line count if this line was removed,
617 // or if the line is a "No newline at end of file" marker.
618 $not_a_line = ($orig_type === '-' || $orig_type === '\\');
619 if ($not_a_line) {
620 continue;
623 if ($is_same || $is_rem) {
624 $n_old++;
627 if ($is_same || $is_add) {
628 $n_new++;
633 $offsets_old = $this->computeOffsets($old_hunks);
634 $offsets_new = $this->computeOffsets($new_hunks);
636 // Highlight lines that were added on each side or removed on the other
637 // side.
638 $highlight_old = array();
639 $highlight_new = array();
640 $last = max(last_key($olds), last_key($news));
641 for ($i = 0; $i <= $last; $i++) {
642 if (isset($olds[$i])) {
643 list($n, $type) = $olds[$i];
644 if ($type == '+' ||
645 ($type == ' ' && isset($news[$i]) && $news[$i][1] != ' ')) {
646 if (isset($offsets_old[$n])) {
647 $highlight_old[] = $offsets_old[$n];
651 if (isset($news[$i])) {
652 list($n, $type) = $news[$i];
653 if ($type == '+' ||
654 ($type == ' ' && isset($olds[$i]) && $olds[$i][1] != ' ')) {
655 if (isset($offsets_new[$n])) {
656 $highlight_new[] = $offsets_new[$n];
662 return array($highlight_old, $highlight_new);
665 public function makeContextDiff(
666 array $hunks,
667 $is_new,
668 $line_number,
669 $line_length,
670 $add_context) {
672 assert_instances_of($hunks, 'DifferentialHunk');
674 $context = array();
676 if ($is_new) {
677 $prefix = '+';
678 } else {
679 $prefix = '-';
682 foreach ($hunks as $hunk) {
683 if ($is_new) {
684 $offset = $hunk->getNewOffset();
685 $length = $hunk->getNewLen();
686 } else {
687 $offset = $hunk->getOldOffset();
688 $length = $hunk->getOldLen();
690 $start = $line_number - $offset;
691 $end = $start + $line_length;
692 // We need to go in if $start == $length, because the last line
693 // might be a "\No newline at end of file" marker, which we want
694 // to show if the additional context is > 0.
695 if ($start <= $length && $end >= 0) {
696 $start = $start - $add_context;
697 $end = $end + $add_context;
698 $hunk_content = array();
699 $hunk_pos = array('-' => 0, '+' => 0);
700 $hunk_offset = array('-' => null, '+' => null);
701 $hunk_last = array('-' => null, '+' => null);
702 foreach (explode("\n", $hunk->getChanges()) as $line) {
703 $in_common = strncmp($line, ' ', 1) === 0;
704 $in_old = strncmp($line, '-', 1) === 0 || $in_common;
705 $in_new = strncmp($line, '+', 1) === 0 || $in_common;
706 $in_selected = strncmp($line, $prefix, 1) === 0;
707 $skip = !$in_selected && !$in_common;
708 if ($hunk_pos[$prefix] <= $end) {
709 if ($start <= $hunk_pos[$prefix]) {
710 if (!$skip || ($hunk_pos[$prefix] != $start &&
711 $hunk_pos[$prefix] != $end)) {
712 if ($in_old) {
713 if ($hunk_offset['-'] === null) {
714 $hunk_offset['-'] = $hunk_pos['-'];
716 $hunk_last['-'] = $hunk_pos['-'];
718 if ($in_new) {
719 if ($hunk_offset['+'] === null) {
720 $hunk_offset['+'] = $hunk_pos['+'];
722 $hunk_last['+'] = $hunk_pos['+'];
725 $hunk_content[] = $line;
728 if ($in_old) { ++$hunk_pos['-']; }
729 if ($in_new) { ++$hunk_pos['+']; }
732 if ($hunk_offset['-'] !== null || $hunk_offset['+'] !== null) {
733 $header = '@@';
734 if ($hunk_offset['-'] !== null) {
735 $header .= ' -'.($hunk->getOldOffset() + $hunk_offset['-']).
736 ','.($hunk_last['-'] - $hunk_offset['-'] + 1);
738 if ($hunk_offset['+'] !== null) {
739 $header .= ' +'.($hunk->getNewOffset() + $hunk_offset['+']).
740 ','.($hunk_last['+'] - $hunk_offset['+'] + 1);
742 $header .= ' @@';
743 $context[] = $header;
744 $context[] = implode("\n", $hunk_content);
748 return implode("\n", $context);
751 private function computeOffsets(array $hunks) {
752 assert_instances_of($hunks, 'DifferentialHunk');
754 $offsets = array();
755 $n = 1;
756 foreach ($hunks as $hunk) {
757 $new_length = $hunk->getNewLen();
758 $new_offset = $hunk->getNewOffset();
760 for ($i = 0; $i < $new_length; $i++) {
761 $offsets[$n] = $new_offset + $i;
762 $n++;
766 return $offsets;
769 private function getIndentDepth($text, $tab_width) {
770 $len = strlen($text);
772 $depth = 0;
773 for ($ii = 0; $ii < $len; $ii++) {
774 $c = $text[$ii];
776 // If this is a space, increase the indent depth by 1.
777 if ($c == ' ') {
778 $depth++;
779 continue;
782 // If this is a tab, increase the indent depth to the next tabstop.
784 // For example, if the tab width is 4, these sequences both lead us to
785 // a visual width of 8, i.e. the cursor will be in the 8th column:
787 // <tab><tab>
788 // <space><tab><space><space><space><tab>
790 if ($c == "\t") {
791 $depth = ($depth + $tab_width);
792 $depth = $depth - ($depth % $tab_width);
793 continue;
796 break;
799 return $depth;
802 private function getCharacterCountForVisualWhitespace(
803 $text,
804 $depth,
805 $tab_width) {
807 // Here, we know the visual indent depth of a line has been increased by
808 // some amount (for example, 6 characters).
810 // We want to find the largest whitespace prefix of the string we can
811 // which still fits into that amount of visual space.
813 // In most cases, this is very easy. For example, if the string has been
814 // indented by two characters and the string begins with two spaces, that's
815 // a perfect match.
817 // However, if the string has been indented by 7 characters, the tab width
818 // is 8, and the string begins with "<space><space><tab>", we can only
819 // mark the two spaces as an indent change. These cases are unusual.
821 $character_depth = 0;
822 $visual_depth = 0;
824 $len = strlen($text);
825 for ($ii = 0; $ii < $len; $ii++) {
826 if ($visual_depth >= $depth) {
827 break;
830 $c = $text[$ii];
832 if ($c == ' ') {
833 $character_depth++;
834 $visual_depth++;
835 continue;
838 if ($c == "\t") {
839 // Figure out how many visual spaces we have until the next tabstop.
840 $tab_visual = ($visual_depth + $tab_width);
841 $tab_visual = $tab_visual - ($tab_visual % $tab_width);
842 $tab_visual = ($tab_visual - $visual_depth);
844 // If this tab would take us over the limit, we're all done.
845 $remaining_depth = ($depth - $visual_depth);
846 if ($remaining_depth < $tab_visual) {
847 break;
850 $character_depth++;
851 $visual_depth += $tab_visual;
852 continue;
855 break;
858 return $character_depth;
861 private function updateChangeTypesForNormalization() {
862 if (!$this->getNormalized()) {
863 return;
866 // If we've parsed based on a normalized diff alignment, we may currently
867 // believe some lines are unchanged when they have actually changed. This
868 // happens when:
870 // - a line changes;
871 // - the change is a kind of change we normalize away when aligning the
872 // diff, like an indentation change;
873 // - we normalize the change away to align the diff; and so
874 // - the old and new copies of the line are now aligned in the new
875 // normalized diff.
877 // Then we end up with an alignment where the two lines that differ only
878 // in some some trivial way are aligned. This is great, and exactly what
879 // we're trying to accomplish by doing all this alignment stuff in the
880 // first place.
882 // However, in this case the correctly-aligned lines will be incorrectly
883 // marked as unchanged because the diff alorithm was fed normalized copies
884 // of the lines, and these copies truly weren't any different.
886 // When lines are aligned and marked identical, but they're not actually
887 // identical, we now mark them as changed. The rest of the processing will
888 // figure out how to render them appropritely.
890 $new = $this->getNewLines();
891 $old = $this->getOldLines();
892 foreach ($old as $key => $o) {
893 $n = $new[$key];
895 if (!$o || !$n) {
896 continue;
899 if ($o['type'] === null && $n['type'] === null) {
900 if ($o['text'] !== $n['text']) {
901 $old[$key]['type'] = '-';
902 $new[$key]['type'] = '+';
907 $this->setOldLines($old);
908 $this->setNewLines($new);