Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/applications/differential/parser/DifferentialHunkParser.php
12256 views
1
<?php
2
3
final class DifferentialHunkParser extends Phobject {
4
5
private $oldLines;
6
private $newLines;
7
private $intraLineDiffs;
8
private $depthOnlyLines;
9
private $visibleLinesMask;
10
private $normalized;
11
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.
16
*
17
* @return dict<int, bool> Map of lines where hunks start, other than line 1.
18
*/
19
public function getHunkStartLines(array $hunks) {
20
assert_instances_of($hunks, 'DifferentialHunk');
21
22
$map = array();
23
foreach ($hunks as $hunk) {
24
$line = $hunk->getOldOffset();
25
if ($line > 1) {
26
$map[$line] = true;
27
}
28
}
29
30
return $map;
31
}
32
33
private function setVisibleLinesMask($mask) {
34
$this->visibleLinesMask = $mask;
35
return $this;
36
}
37
public function getVisibleLinesMask() {
38
if ($this->visibleLinesMask === null) {
39
throw new PhutilInvalidStateException('generateVisibleLinesMask');
40
}
41
return $this->visibleLinesMask;
42
}
43
44
private function setIntraLineDiffs($intra_line_diffs) {
45
$this->intraLineDiffs = $intra_line_diffs;
46
return $this;
47
}
48
public function getIntraLineDiffs() {
49
if ($this->intraLineDiffs === null) {
50
throw new PhutilInvalidStateException('generateIntraLineDiffs');
51
}
52
return $this->intraLineDiffs;
53
}
54
55
private function setNewLines($new_lines) {
56
$this->newLines = $new_lines;
57
return $this;
58
}
59
public function getNewLines() {
60
if ($this->newLines === null) {
61
throw new PhutilInvalidStateException('parseHunksForLineData');
62
}
63
return $this->newLines;
64
}
65
66
private function setOldLines($old_lines) {
67
$this->oldLines = $old_lines;
68
return $this;
69
}
70
public function getOldLines() {
71
if ($this->oldLines === null) {
72
throw new PhutilInvalidStateException('parseHunksForLineData');
73
}
74
return $this->oldLines;
75
}
76
77
public function getOldLineTypeMap() {
78
$map = array();
79
$old = $this->getOldLines();
80
foreach ($old as $o) {
81
if (!$o) {
82
continue;
83
}
84
$map[$o['line']] = $o['type'];
85
}
86
return $map;
87
}
88
89
public function setOldLineTypeMap(array $map) {
90
$lines = $this->getOldLines();
91
foreach ($lines as $key => $data) {
92
$lines[$key]['type'] = idx($map, $data['line']);
93
}
94
$this->oldLines = $lines;
95
return $this;
96
}
97
98
public function getNewLineTypeMap() {
99
$map = array();
100
$new = $this->getNewLines();
101
foreach ($new as $n) {
102
if (!$n) {
103
continue;
104
}
105
$map[$n['line']] = $n['type'];
106
}
107
return $map;
108
}
109
110
public function setNewLineTypeMap(array $map) {
111
$lines = $this->getNewLines();
112
foreach ($lines as $key => $data) {
113
$lines[$key]['type'] = idx($map, $data['line']);
114
}
115
$this->newLines = $lines;
116
return $this;
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
}
127
128
public function setNormalized($normalized) {
129
$this->normalized = $normalized;
130
return $this;
131
}
132
133
public function getNormalized() {
134
return $this->normalized;
135
}
136
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;
142
}
143
}
144
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;
149
}
150
}
151
152
// This is an empty file.
153
return false;
154
}
155
156
/**
157
* Returns true if the hunks change anything, including whitespace.
158
*/
159
public function getHasAnyChanges() {
160
return $this->getHasChanges('any');
161
}
162
163
private function getHasChanges($filter) {
164
if ($filter !== 'any' && $filter !== 'text') {
165
throw new Exception(pht("Unknown change filter '%s'.", $filter));
166
}
167
168
$old = $this->getOldLines();
169
$new = $this->getNewLines();
170
171
$is_any = ($filter === 'any');
172
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;
180
}
181
182
if ($o['type'] !== $n['type']) {
183
return true;
184
}
185
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;
192
}
193
}
194
}
195
196
// No changes anywhere in the file.
197
return false;
198
}
199
200
201
/**
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.
205
*
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.
209
*
210
* Anyhoo, this function is not particularly well-named but I try.
211
*
212
* NOTE: this function must be called after
213
* @{method:parseHunksForLineData}.
214
*/
215
public function reparseHunksForSpecialAttributes() {
216
$rebuild_old = array();
217
$rebuild_new = array();
218
219
$old_lines = array_reverse($this->getOldLines());
220
$new_lines = array_reverse($this->getNewLines());
221
222
while (count($old_lines) || count($new_lines)) {
223
$old_line_data = array_pop($old_lines);
224
$new_line_data = array_pop($new_lines);
225
226
if ($old_line_data) {
227
$o_type = $old_line_data['type'];
228
} else {
229
$o_type = null;
230
}
231
232
if ($new_line_data) {
233
$n_type = $new_line_data['type'];
234
} else {
235
$n_type = null;
236
}
237
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);
244
}
245
continue;
246
}
247
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);
254
}
255
continue;
256
}
257
258
$rebuild_old[] = $old_line_data;
259
$rebuild_new[] = $new_line_data;
260
}
261
262
$this->setOldLines($rebuild_old);
263
$this->setNewLines($rebuild_new);
264
265
$this->updateChangeTypesForNormalization();
266
267
return $this;
268
}
269
270
public function generateIntraLineDiffs() {
271
$old = $this->getOldLines();
272
$new = $this->getNewLines();
273
274
$diffs = array();
275
$depth_only = array();
276
foreach ($old as $key => $o) {
277
$n = $new[$key];
278
279
if (!$o || !$n) {
280
continue;
281
}
282
283
if ($o['type'] != $n['type']) {
284
$o_segments = array();
285
$n_segments = array();
286
$tab_width = 8;
287
288
$o_text = $o['text'];
289
$n_text = $n['text'];
290
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);
294
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,
306
);
307
}
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,
319
);
320
}
321
}
322
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;
329
}
330
}
331
332
$intraline_segments = ArcanistDiffUtils::generateIntralineDiff(
333
$o_text,
334
$n_text);
335
336
foreach ($intraline_segments[0] as $o_segment) {
337
$o_segments[] = $o_segment;
338
}
339
340
foreach ($intraline_segments[1] as $n_segment) {
341
$n_segments[] = $n_segment;
342
}
343
344
$diffs[$key] = array(
345
$o_segments,
346
$n_segments,
347
);
348
}
349
}
350
351
$this->setIntraLineDiffs($diffs);
352
$this->setDepthOnlyLines($depth_only);
353
354
return $this;
355
}
356
357
public function generateVisibleBlocksMask($lines_context) {
358
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.
362
363
$old = $this->getOldLines();
364
$new = $this->getNewLines();
365
366
$length = max(count($old), count($new));
367
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']);
372
373
$visible_lines[$ii] = ($old_visible || $new_visible);
374
}
375
376
$mask = array();
377
$reveal_cursor = -1;
378
for ($ii = 0; $ii < $length; $ii++) {
379
380
// If this line isn't visible, it isn't going to reveal anything.
381
if (!$visible_lines[$ii]) {
382
383
// If it hasn't been revealed by a nearby line, mark it as masked.
384
if (empty($mask[$ii])) {
385
$mask[$ii] = false;
386
}
387
388
continue;
389
}
390
391
// If this line is visible, reveal all the lines nearby.
392
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);
396
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.
400
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);
405
406
// Reveal the remaining unrevealed lines.
407
for ($jj = $min_reveal; $jj <= $max_reveal; $jj++) {
408
$mask[$jj] = true;
409
}
410
411
// Move the cursor to the next line which may still need to be revealed.
412
$reveal_cursor = $max_reveal + 1;
413
}
414
415
$this->setVisibleLinesMask($mask);
416
417
return $mask;
418
}
419
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();
427
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;
436
}
437
if ($visible && $cursor > 0) {
438
$mask[$cursor] = 1;
439
}
440
}
441
442
$this->setVisibleLinesMask($mask);
443
444
return $this;
445
}
446
447
public function getOldCorpus() {
448
return $this->getCorpus($this->getOldLines());
449
}
450
451
public function getNewCorpus() {
452
return $this->getCorpus($this->getNewLines());
453
}
454
455
private function getCorpus(array $lines) {
456
457
$corpus = array();
458
foreach ($lines as $l) {
459
if ($l === null) {
460
$corpus[] = "\n";
461
continue;
462
}
463
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'];
471
}
472
}
473
}
474
return $corpus;
475
}
476
477
public function parseHunksForLineData(array $hunks) {
478
assert_instances_of($hunks, 'DifferentialHunk');
479
480
$old_lines = array();
481
$new_lines = array();
482
foreach ($hunks as $hunk) {
483
$lines = $hunk->getSplitLines();
484
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));
517
}
518
} else {
519
$line_type_map[$line_index] = null;
520
$line_text[$line_index] = '';
521
}
522
}
523
524
$old_line = $hunk->getOldOffset();
525
$new_line = $hunk->getNewOffset();
526
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,
534
);
535
if ($type == '\\') {
536
$type = $line_type_map[$cursor - 1];
537
$data['text'] = ltrim($data['text']);
538
}
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;
556
}
557
}
558
}
559
560
$this->setOldLines($old_lines);
561
$this->setNewLines($new_lines);
562
563
return $this;
564
}
565
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');
573
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 === '-');
588
589
$orig_type = $line[1]; // Change type in the original diff.
590
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;
597
}
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));
614
}
615
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;
621
}
622
623
if ($is_same || $is_rem) {
624
$n_old++;
625
}
626
627
if ($is_same || $is_add) {
628
$n_new++;
629
}
630
}
631
}
632
633
$offsets_old = $this->computeOffsets($old_hunks);
634
$offsets_new = $this->computeOffsets($new_hunks);
635
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];
648
}
649
}
650
}
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];
657
}
658
}
659
}
660
}
661
662
return array($highlight_old, $highlight_new);
663
}
664
665
public function makeContextDiff(
666
array $hunks,
667
$is_new,
668
$line_number,
669
$line_length,
670
$add_context) {
671
672
assert_instances_of($hunks, 'DifferentialHunk');
673
674
$context = array();
675
676
if ($is_new) {
677
$prefix = '+';
678
} else {
679
$prefix = '-';
680
}
681
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();
689
}
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['-'];
715
}
716
$hunk_last['-'] = $hunk_pos['-'];
717
}
718
if ($in_new) {
719
if ($hunk_offset['+'] === null) {
720
$hunk_offset['+'] = $hunk_pos['+'];
721
}
722
$hunk_last['+'] = $hunk_pos['+'];
723
}
724
725
$hunk_content[] = $line;
726
}
727
}
728
if ($in_old) { ++$hunk_pos['-']; }
729
if ($in_new) { ++$hunk_pos['+']; }
730
}
731
}
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);
737
}
738
if ($hunk_offset['+'] !== null) {
739
$header .= ' +'.($hunk->getNewOffset() + $hunk_offset['+']).
740
','.($hunk_last['+'] - $hunk_offset['+'] + 1);
741
}
742
$header .= ' @@';
743
$context[] = $header;
744
$context[] = implode("\n", $hunk_content);
745
}
746
}
747
}
748
return implode("\n", $context);
749
}
750
751
private function computeOffsets(array $hunks) {
752
assert_instances_of($hunks, 'DifferentialHunk');
753
754
$offsets = array();
755
$n = 1;
756
foreach ($hunks as $hunk) {
757
$new_length = $hunk->getNewLen();
758
$new_offset = $hunk->getNewOffset();
759
760
for ($i = 0; $i < $new_length; $i++) {
761
$offsets[$n] = $new_offset + $i;
762
$n++;
763
}
764
}
765
766
return $offsets;
767
}
768
769
private function getIndentDepth($text, $tab_width) {
770
$len = strlen($text);
771
772
$depth = 0;
773
for ($ii = 0; $ii < $len; $ii++) {
774
$c = $text[$ii];
775
776
// If this is a space, increase the indent depth by 1.
777
if ($c == ' ') {
778
$depth++;
779
continue;
780
}
781
782
// If this is a tab, increase the indent depth to the next tabstop.
783
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:
786
//
787
// <tab><tab>
788
// <space><tab><space><space><space><tab>
789
790
if ($c == "\t") {
791
$depth = ($depth + $tab_width);
792
$depth = $depth - ($depth % $tab_width);
793
continue;
794
}
795
796
break;
797
}
798
799
return $depth;
800
}
801
802
private function getCharacterCountForVisualWhitespace(
803
$text,
804
$depth,
805
$tab_width) {
806
807
// Here, we know the visual indent depth of a line has been increased by
808
// some amount (for example, 6 characters).
809
810
// We want to find the largest whitespace prefix of the string we can
811
// which still fits into that amount of visual space.
812
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.
816
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.
820
821
$character_depth = 0;
822
$visual_depth = 0;
823
824
$len = strlen($text);
825
for ($ii = 0; $ii < $len; $ii++) {
826
if ($visual_depth >= $depth) {
827
break;
828
}
829
830
$c = $text[$ii];
831
832
if ($c == ' ') {
833
$character_depth++;
834
$visual_depth++;
835
continue;
836
}
837
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);
843
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;
848
}
849
850
$character_depth++;
851
$visual_depth += $tab_visual;
852
continue;
853
}
854
855
break;
856
}
857
858
return $character_depth;
859
}
860
861
private function updateChangeTypesForNormalization() {
862
if (!$this->getNormalized()) {
863
return;
864
}
865
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:
869
//
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.
876
//
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.
881
//
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.
885
//
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.
889
890
$new = $this->getNewLines();
891
$old = $this->getOldLines();
892
foreach ($old as $key => $o) {
893
$n = $new[$key];
894
895
if (!$o || !$n) {
896
continue;
897
}
898
899
if ($o['type'] === null && $n['type'] === null) {
900
if ($o['text'] !== $n['text']) {
901
$old[$key]['type'] = '-';
902
$new[$key]['type'] = '+';
903
}
904
}
905
}
906
907
$this->setOldLines($old);
908
$this->setNewLines($new);
909
}
910
911
912
}
913
914