Fossil SCM

Coloring on side-by-side diffs allows up to 8 separate segments of change. Incremental check-in - not everything is working correctly.

drh 2021-08-27 16:48 UTC diff-color-enhancements
Commit 925399da07045aa4bdd29f764150af55c841ef94dc6fc6e42243c79a5073ae0d
2 files changed +105 -32 +4 -1
+105 -32
--- src/diff.c
+++ src/diff.c
@@ -513,13 +513,14 @@
513513
}
514514
if( html ) blob_append(pOut, "</pre>\n", -1);
515515
}
516516
517517
/*
518
-** Maximum number of change regions per line
518
+** Limits for the intra-line diffing.
519519
*/
520
-#define SBS_MXN 4
520
+#define SBS_MXN 8 /* Maximum number of change regions per line of text */
521
+#define SBS_CSN 8 /* Maximum number of change spans across a change region */
521522
522523
/*
523524
** Status of a single output line
524525
*/
525526
typedef struct SbsLine SbsLine;
@@ -527,17 +528,32 @@
527528
Blob *apCols[5]; /* Array of pointers to output columns */
528529
int width; /* Maximum width of a column in the output */
529530
unsigned char escHtml; /* True to escape html characters */
530531
struct SbsMark {
531532
int iStart; /* Write zTag prior to character iStart */
532
- const char *zTag; /* A <span> tag for coloration */
533533
int iEnd; /* Write </span> prior to character iEnd */
534
+ const char *zTag; /* A <span> tag for coloration */
534535
} a[SBS_MXN]; /* Change regions */
535536
int n; /* Number of change regions used */
536537
ReCompiled *pRe; /* Only colorize matching lines, if not NULL */
537538
};
538539
540
+/*
541
+** A description of zero or more (up to SBS_CSN) areas of commonality
542
+** between two lines of text.
543
+*/
544
+typedef struct ChangeSpan ChangeSpan;
545
+struct ChangeSpan {
546
+ int n; /* Number of change spans */
547
+ struct Span {
548
+ int iStart1; /* Byte offset to start of change on the left */
549
+ int iLen1; /* Length of left change span in bytes */
550
+ int iStart2; /* Byte offset to start of change span on the right */
551
+ int iLen2; /* Length of right change span in bytes */
552
+ int isMin; /* True if this span is known to have no useful subdivs */
553
+ } a[SBS_CSN]; /* Array of change spans, sorted order */
554
+};
539555
540556
/*
541557
** Column indices for SbsLine.apCols[]
542558
*/
543559
#define SBS_LNA 0 /* Left line number */
@@ -747,10 +763,64 @@
747763
}
748764
}
749765
}
750766
return rc;
751767
}
768
+
769
+/*
770
+** Find the smallest spans that different between two text strings that
771
+** are known to be different on both ends.
772
+*/
773
+static int textChangeSpans(
774
+ const char *zLeft, int nA, /* String on the left */
775
+ const char *zRight, int nB, /* String on the right */
776
+ ChangeSpan *p /* Write results here */
777
+){
778
+ p->n = 1;
779
+ p->a[0].iStart1 = 0;
780
+ p->a[0].iLen1 = nA;
781
+ p->a[0].iStart2 = 0;
782
+ p->a[0].iLen2 = nB;
783
+ p->a[0].isMin = 0;
784
+ while( p->n<SBS_CSN ){
785
+ int mxi = -1;
786
+ int mxLen = -1;
787
+ int x, i;
788
+ int aLCS[4];
789
+ struct Span *a, *b;
790
+ for(i=0; i<p->n; i++){
791
+ if( p->a[i].isMin ) continue;
792
+ x = p->a[i].iLen1;
793
+ if( p->a[i].iLen2<x ) x = p->a[i].iLen2;
794
+ if( x>mxLen ){
795
+ mxLen = x;
796
+ mxi = i;
797
+ }
798
+ }
799
+ if( mxLen<6 ) break;
800
+ x = textLCS(zLeft + p->a[mxi].iStart1, p->a[mxi].iLen1,
801
+ zRight + p->a[mxi].iStart2, p->a[mxi].iLen2, aLCS);
802
+ if( x==0 ){
803
+ p->a[mxi].isMin = 1;
804
+ continue;
805
+ }
806
+ a = p->a+mxi;
807
+ b = a+1;
808
+ if( mxi<p->n-1 ){
809
+ memmove(b+1, b, sizeof(*b)*(p->n-mxi-1));
810
+ }
811
+ p->n++;
812
+ b->iStart1 = a->iStart1 + aLCS[1];
813
+ b->iLen1 = a->iLen1 - aLCS[1];
814
+ a->iLen1 = aLCS[0];
815
+ b->iStart2 = a->iStart2 + aLCS[3];
816
+ b->iLen2 = a->iLen2 - aLCS[3];
817
+ a->iLen2 = aLCS[2];
818
+ b->isMin = 0;
819
+ }
820
+ return p->n;
821
+}
752822
753823
/*
754824
** Try to shift a[0].iStart as far as possible to the left.
755825
*/
756826
static void sbsShiftLeft(SbsLine *p, const char *z){
@@ -762,14 +832,13 @@
762832
p->a[0].iEnd--;
763833
}
764834
}
765835
766836
/*
767
-** Simplify iStart and iStart2:
837
+** Simplify the diff-marks in a single line.
768838
**
769
-** * If iStart is a null-change then move iStart2 into iStart
770
-** * Make sure any null-changes are in canonoical form.
839
+** * Remove any null (zero-length) diff marks.
771840
** * Make sure all changes are at character boundaries for
772841
** multi-byte characters.
773842
*/
774843
static void sbsSimplifyLine(SbsLine *p, const char *z){
775844
int i, j;
@@ -809,11 +878,11 @@
809878
int nSuffix; /* Length of common suffix */
810879
const char *zLeft; /* Text of the left line */
811880
const char *zRight; /* Text of the right line */
812881
int nLeftDiff; /* nLeft - nPrefix - nSuffix */
813882
int nRightDiff; /* nRight - nPrefix - nSuffix */
814
- int aLCS[4]; /* Bounds of common middle segment */
883
+ ChangeSpan CSpan; /* Set of changes on a line */
815884
816885
nLeft = pLeft->n;
817886
zLeft = pLeft->z;
818887
nRight = pRight->n;
819888
zRight = pRight->z;
@@ -915,41 +984,45 @@
915984
nLeftDiff = nLeft - nSuffix - nPrefix;
916985
nRightDiff = nRight - nSuffix - nPrefix;
917986
if( p->escHtml
918987
&& nLeftDiff >= 6
919988
&& nRightDiff >= 6
920
- && textLCS(&zLeft[nPrefix], nLeftDiff, &zRight[nPrefix], nRightDiff, aLCS)
989
+ && textChangeSpans(&zLeft[nPrefix], nLeftDiff,
990
+ &zRight[nPrefix], nRightDiff, &CSpan)>1
921991
){
992
+ int i, j;
922993
sbsWriteLineno(p, lnLeft, SBS_LNA);
923
- p->a[0].iStart = nPrefix;
924
- p->a[0].iEnd = nPrefix + aLCS[0];
925
- if( aLCS[2]==0 ){
926
- sbsShiftLeft(p, pLeft->z);
927
- p->a[0].zTag = zClassRm;
928
- }else{
929
- p->a[0].zTag = zClassChng;
930
- }
931
- p->a[1].iStart = nPrefix + aLCS[1];
932
- p->a[1].iEnd = nLeft - nSuffix;
933
- p->a[1].zTag = aLCS[3]==nRightDiff ? zClassRm : zClassChng;
934
- p->n = 2;
994
+ for(i=j=0; i<CSpan.n; i++){
995
+ if( CSpan.a[i].iLen1==0 ) continue;
996
+ p->a[j].iStart = nPrefix + CSpan.a[i].iStart1;
997
+ p->a[j].iEnd = p->a[i].iStart + CSpan.a[i].iLen1;
998
+ if( CSpan.a[i].iLen2==0 ){
999
+ if( i==0 ) sbsShiftLeft(p, zLeft);
1000
+ p->a[j].zTag = zClassRm;
1001
+ }else{
1002
+ p->a[j].zTag = zClassChng;
1003
+ }
1004
+ j++;
1005
+ }
1006
+ p->n = j;
9351007
sbsSimplifyLine(p, zLeft);
9361008
sbsWriteText(p, pLeft, SBS_TXTA);
9371009
sbsWriteMarker(p, " | ", "|");
9381010
sbsWriteLineno(p, lnRight, SBS_LNB);
939
- p->a[0].iStart = nPrefix;
940
- p->a[0].iEnd = nPrefix + aLCS[2];
941
- if( aLCS[0]==0 ){
942
- sbsShiftLeft(p, pRight->z);
943
- p->a[0].zTag = zClassAdd;
944
- }else{
945
- p->a[0].zTag = zClassChng;
946
- }
947
- p->a[1].iStart = nPrefix + aLCS[3];
948
- p->a[1].iEnd = nRight - nSuffix;
949
- p->a[1].zTag = aLCS[1]==nLeftDiff ? zClassAdd : zClassChng;
950
- p->n = 2;
1011
+ for(i=j=0; i<CSpan.n; i++){
1012
+ if( CSpan.a[i].iLen2==0 ) continue;
1013
+ p->a[j].iStart = nPrefix + CSpan.a[i].iStart2;
1014
+ p->a[j].iEnd = p->a[i].iStart + CSpan.a[i].iLen2;
1015
+ if( CSpan.a[i].iLen1==0 ){
1016
+ if( i==0 ) sbsShiftLeft(p, zRight);
1017
+ p->a[j].zTag = zClassAdd;
1018
+ }else{
1019
+ p->a[j].zTag = zClassChng;
1020
+ }
1021
+ j++;
1022
+ }
1023
+ p->n = j;
9511024
sbsSimplifyLine(p, zRight);
9521025
sbsWriteText(p, pRight, SBS_TXTB);
9531026
return;
9541027
}
9551028
9561029
--- src/diff.c
+++ src/diff.c
@@ -513,13 +513,14 @@
513 }
514 if( html ) blob_append(pOut, "</pre>\n", -1);
515 }
516
517 /*
518 ** Maximum number of change regions per line
519 */
520 #define SBS_MXN 4
 
521
522 /*
523 ** Status of a single output line
524 */
525 typedef struct SbsLine SbsLine;
@@ -527,17 +528,32 @@
527 Blob *apCols[5]; /* Array of pointers to output columns */
528 int width; /* Maximum width of a column in the output */
529 unsigned char escHtml; /* True to escape html characters */
530 struct SbsMark {
531 int iStart; /* Write zTag prior to character iStart */
532 const char *zTag; /* A <span> tag for coloration */
533 int iEnd; /* Write </span> prior to character iEnd */
 
534 } a[SBS_MXN]; /* Change regions */
535 int n; /* Number of change regions used */
536 ReCompiled *pRe; /* Only colorize matching lines, if not NULL */
537 };
538
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
539
540 /*
541 ** Column indices for SbsLine.apCols[]
542 */
543 #define SBS_LNA 0 /* Left line number */
@@ -747,10 +763,64 @@
747 }
748 }
749 }
750 return rc;
751 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
752
753 /*
754 ** Try to shift a[0].iStart as far as possible to the left.
755 */
756 static void sbsShiftLeft(SbsLine *p, const char *z){
@@ -762,14 +832,13 @@
762 p->a[0].iEnd--;
763 }
764 }
765
766 /*
767 ** Simplify iStart and iStart2:
768 **
769 ** * If iStart is a null-change then move iStart2 into iStart
770 ** * Make sure any null-changes are in canonoical form.
771 ** * Make sure all changes are at character boundaries for
772 ** multi-byte characters.
773 */
774 static void sbsSimplifyLine(SbsLine *p, const char *z){
775 int i, j;
@@ -809,11 +878,11 @@
809 int nSuffix; /* Length of common suffix */
810 const char *zLeft; /* Text of the left line */
811 const char *zRight; /* Text of the right line */
812 int nLeftDiff; /* nLeft - nPrefix - nSuffix */
813 int nRightDiff; /* nRight - nPrefix - nSuffix */
814 int aLCS[4]; /* Bounds of common middle segment */
815
816 nLeft = pLeft->n;
817 zLeft = pLeft->z;
818 nRight = pRight->n;
819 zRight = pRight->z;
@@ -915,41 +984,45 @@
915 nLeftDiff = nLeft - nSuffix - nPrefix;
916 nRightDiff = nRight - nSuffix - nPrefix;
917 if( p->escHtml
918 && nLeftDiff >= 6
919 && nRightDiff >= 6
920 && textLCS(&zLeft[nPrefix], nLeftDiff, &zRight[nPrefix], nRightDiff, aLCS)
 
921 ){
 
922 sbsWriteLineno(p, lnLeft, SBS_LNA);
923 p->a[0].iStart = nPrefix;
924 p->a[0].iEnd = nPrefix + aLCS[0];
925 if( aLCS[2]==0 ){
926 sbsShiftLeft(p, pLeft->z);
927 p->a[0].zTag = zClassRm;
928 }else{
929 p->a[0].zTag = zClassChng;
930 }
931 p->a[1].iStart = nPrefix + aLCS[1];
932 p->a[1].iEnd = nLeft - nSuffix;
933 p->a[1].zTag = aLCS[3]==nRightDiff ? zClassRm : zClassChng;
934 p->n = 2;
 
935 sbsSimplifyLine(p, zLeft);
936 sbsWriteText(p, pLeft, SBS_TXTA);
937 sbsWriteMarker(p, " | ", "|");
938 sbsWriteLineno(p, lnRight, SBS_LNB);
939 p->a[0].iStart = nPrefix;
940 p->a[0].iEnd = nPrefix + aLCS[2];
941 if( aLCS[0]==0 ){
942 sbsShiftLeft(p, pRight->z);
943 p->a[0].zTag = zClassAdd;
944 }else{
945 p->a[0].zTag = zClassChng;
946 }
947 p->a[1].iStart = nPrefix + aLCS[3];
948 p->a[1].iEnd = nRight - nSuffix;
949 p->a[1].zTag = aLCS[1]==nLeftDiff ? zClassAdd : zClassChng;
950 p->n = 2;
 
951 sbsSimplifyLine(p, zRight);
952 sbsWriteText(p, pRight, SBS_TXTB);
953 return;
954 }
955
956
--- src/diff.c
+++ src/diff.c
@@ -513,13 +513,14 @@
513 }
514 if( html ) blob_append(pOut, "</pre>\n", -1);
515 }
516
517 /*
518 ** Limits for the intra-line diffing.
519 */
520 #define SBS_MXN 8 /* Maximum number of change regions per line of text */
521 #define SBS_CSN 8 /* Maximum number of change spans across a change region */
522
523 /*
524 ** Status of a single output line
525 */
526 typedef struct SbsLine SbsLine;
@@ -527,17 +528,32 @@
528 Blob *apCols[5]; /* Array of pointers to output columns */
529 int width; /* Maximum width of a column in the output */
530 unsigned char escHtml; /* True to escape html characters */
531 struct SbsMark {
532 int iStart; /* Write zTag prior to character iStart */
 
533 int iEnd; /* Write </span> prior to character iEnd */
534 const char *zTag; /* A <span> tag for coloration */
535 } a[SBS_MXN]; /* Change regions */
536 int n; /* Number of change regions used */
537 ReCompiled *pRe; /* Only colorize matching lines, if not NULL */
538 };
539
540 /*
541 ** A description of zero or more (up to SBS_CSN) areas of commonality
542 ** between two lines of text.
543 */
544 typedef struct ChangeSpan ChangeSpan;
545 struct ChangeSpan {
546 int n; /* Number of change spans */
547 struct Span {
548 int iStart1; /* Byte offset to start of change on the left */
549 int iLen1; /* Length of left change span in bytes */
550 int iStart2; /* Byte offset to start of change span on the right */
551 int iLen2; /* Length of right change span in bytes */
552 int isMin; /* True if this span is known to have no useful subdivs */
553 } a[SBS_CSN]; /* Array of change spans, sorted order */
554 };
555
556 /*
557 ** Column indices for SbsLine.apCols[]
558 */
559 #define SBS_LNA 0 /* Left line number */
@@ -747,10 +763,64 @@
763 }
764 }
765 }
766 return rc;
767 }
768
769 /*
770 ** Find the smallest spans that different between two text strings that
771 ** are known to be different on both ends.
772 */
773 static int textChangeSpans(
774 const char *zLeft, int nA, /* String on the left */
775 const char *zRight, int nB, /* String on the right */
776 ChangeSpan *p /* Write results here */
777 ){
778 p->n = 1;
779 p->a[0].iStart1 = 0;
780 p->a[0].iLen1 = nA;
781 p->a[0].iStart2 = 0;
782 p->a[0].iLen2 = nB;
783 p->a[0].isMin = 0;
784 while( p->n<SBS_CSN ){
785 int mxi = -1;
786 int mxLen = -1;
787 int x, i;
788 int aLCS[4];
789 struct Span *a, *b;
790 for(i=0; i<p->n; i++){
791 if( p->a[i].isMin ) continue;
792 x = p->a[i].iLen1;
793 if( p->a[i].iLen2<x ) x = p->a[i].iLen2;
794 if( x>mxLen ){
795 mxLen = x;
796 mxi = i;
797 }
798 }
799 if( mxLen<6 ) break;
800 x = textLCS(zLeft + p->a[mxi].iStart1, p->a[mxi].iLen1,
801 zRight + p->a[mxi].iStart2, p->a[mxi].iLen2, aLCS);
802 if( x==0 ){
803 p->a[mxi].isMin = 1;
804 continue;
805 }
806 a = p->a+mxi;
807 b = a+1;
808 if( mxi<p->n-1 ){
809 memmove(b+1, b, sizeof(*b)*(p->n-mxi-1));
810 }
811 p->n++;
812 b->iStart1 = a->iStart1 + aLCS[1];
813 b->iLen1 = a->iLen1 - aLCS[1];
814 a->iLen1 = aLCS[0];
815 b->iStart2 = a->iStart2 + aLCS[3];
816 b->iLen2 = a->iLen2 - aLCS[3];
817 a->iLen2 = aLCS[2];
818 b->isMin = 0;
819 }
820 return p->n;
821 }
822
823 /*
824 ** Try to shift a[0].iStart as far as possible to the left.
825 */
826 static void sbsShiftLeft(SbsLine *p, const char *z){
@@ -762,14 +832,13 @@
832 p->a[0].iEnd--;
833 }
834 }
835
836 /*
837 ** Simplify the diff-marks in a single line.
838 **
839 ** * Remove any null (zero-length) diff marks.
 
840 ** * Make sure all changes are at character boundaries for
841 ** multi-byte characters.
842 */
843 static void sbsSimplifyLine(SbsLine *p, const char *z){
844 int i, j;
@@ -809,11 +878,11 @@
878 int nSuffix; /* Length of common suffix */
879 const char *zLeft; /* Text of the left line */
880 const char *zRight; /* Text of the right line */
881 int nLeftDiff; /* nLeft - nPrefix - nSuffix */
882 int nRightDiff; /* nRight - nPrefix - nSuffix */
883 ChangeSpan CSpan; /* Set of changes on a line */
884
885 nLeft = pLeft->n;
886 zLeft = pLeft->z;
887 nRight = pRight->n;
888 zRight = pRight->z;
@@ -915,41 +984,45 @@
984 nLeftDiff = nLeft - nSuffix - nPrefix;
985 nRightDiff = nRight - nSuffix - nPrefix;
986 if( p->escHtml
987 && nLeftDiff >= 6
988 && nRightDiff >= 6
989 && textChangeSpans(&zLeft[nPrefix], nLeftDiff,
990 &zRight[nPrefix], nRightDiff, &CSpan)>1
991 ){
992 int i, j;
993 sbsWriteLineno(p, lnLeft, SBS_LNA);
994 for(i=j=0; i<CSpan.n; i++){
995 if( CSpan.a[i].iLen1==0 ) continue;
996 p->a[j].iStart = nPrefix + CSpan.a[i].iStart1;
997 p->a[j].iEnd = p->a[i].iStart + CSpan.a[i].iLen1;
998 if( CSpan.a[i].iLen2==0 ){
999 if( i==0 ) sbsShiftLeft(p, zLeft);
1000 p->a[j].zTag = zClassRm;
1001 }else{
1002 p->a[j].zTag = zClassChng;
1003 }
1004 j++;
1005 }
1006 p->n = j;
1007 sbsSimplifyLine(p, zLeft);
1008 sbsWriteText(p, pLeft, SBS_TXTA);
1009 sbsWriteMarker(p, " | ", "|");
1010 sbsWriteLineno(p, lnRight, SBS_LNB);
1011 for(i=j=0; i<CSpan.n; i++){
1012 if( CSpan.a[i].iLen2==0 ) continue;
1013 p->a[j].iStart = nPrefix + CSpan.a[i].iStart2;
1014 p->a[j].iEnd = p->a[i].iStart + CSpan.a[i].iLen2;
1015 if( CSpan.a[i].iLen1==0 ){
1016 if( i==0 ) sbsShiftLeft(p, zRight);
1017 p->a[j].zTag = zClassAdd;
1018 }else{
1019 p->a[j].zTag = zClassChng;
1020 }
1021 j++;
1022 }
1023 p->n = j;
1024 sbsSimplifyLine(p, zRight);
1025 sbsWriteText(p, pRight, SBS_TXTB);
1026 return;
1027 }
1028
1029
--- test/diff-test-1.wiki
+++ test/diff-test-1.wiki
@@ -20,11 +20,14 @@
2020
The edit of a line with multibyte characters is the first chunk.
2121
* <a href="../../../fdiff?v1=57b0d8183cab0e3d&v2=37b3ef49d73cdfe6"
2222
target="testwindow">Large diff of sqlite3.c</a>. This diff was very
2323
slow prior to the performance enhancement change [9e15437e97].
2424
* <a href="../../../info/bda00cbada#chunk49" target="testwindow">
25
- A difficult indentation change.</a>
25
+ A difficult indentation change.</a> UPDATE: Notice also the improved
26
+ multi-segment update marks on lines 122648 and 122763 on the new side.
27
+ * <a href="../../../fdiff?v1=bc8100c9ee01b8c2&v2=1d2acc1a2a65c2bf#chunk42"
28
+ target="testwindow">Inverse of the previous.</a>
2629
* <a href="../../../fdiff?v1=955cc67ace8fb622&v2=e2e1c87b86664b45#chunk13"
2730
target="testwindow">Another tricky indentation.</a> Notice especially
2831
lines 59398 and 59407 on the left.
2932
* <a href="../../../fdiff?v2=955cc67ace8fb622&v1=e2e1c87b86664b45#chunk13"
3033
target="testwindow">Inverse of the previous.</a>
3134
--- test/diff-test-1.wiki
+++ test/diff-test-1.wiki
@@ -20,11 +20,14 @@
20 The edit of a line with multibyte characters is the first chunk.
21 * <a href="../../../fdiff?v1=57b0d8183cab0e3d&v2=37b3ef49d73cdfe6"
22 target="testwindow">Large diff of sqlite3.c</a>. This diff was very
23 slow prior to the performance enhancement change [9e15437e97].
24 * <a href="../../../info/bda00cbada#chunk49" target="testwindow">
25 A difficult indentation change.</a>
 
 
 
26 * <a href="../../../fdiff?v1=955cc67ace8fb622&v2=e2e1c87b86664b45#chunk13"
27 target="testwindow">Another tricky indentation.</a> Notice especially
28 lines 59398 and 59407 on the left.
29 * <a href="../../../fdiff?v2=955cc67ace8fb622&v1=e2e1c87b86664b45#chunk13"
30 target="testwindow">Inverse of the previous.</a>
31
--- test/diff-test-1.wiki
+++ test/diff-test-1.wiki
@@ -20,11 +20,14 @@
20 The edit of a line with multibyte characters is the first chunk.
21 * <a href="../../../fdiff?v1=57b0d8183cab0e3d&v2=37b3ef49d73cdfe6"
22 target="testwindow">Large diff of sqlite3.c</a>. This diff was very
23 slow prior to the performance enhancement change [9e15437e97].
24 * <a href="../../../info/bda00cbada#chunk49" target="testwindow">
25 A difficult indentation change.</a> UPDATE: Notice also the improved
26 multi-segment update marks on lines 122648 and 122763 on the new side.
27 * <a href="../../../fdiff?v1=bc8100c9ee01b8c2&v2=1d2acc1a2a65c2bf#chunk42"
28 target="testwindow">Inverse of the previous.</a>
29 * <a href="../../../fdiff?v1=955cc67ace8fb622&v2=e2e1c87b86664b45#chunk13"
30 target="testwindow">Another tricky indentation.</a> Notice especially
31 lines 59398 and 59407 on the left.
32 * <a href="../../../fdiff?v2=955cc67ace8fb622&v1=e2e1c87b86664b45#chunk13"
33 target="testwindow">Inverse of the previous.</a>
34

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button