Index: skins/ardoise/css.txt ================================================================== --- skins/ardoise/css.txt +++ skins/ardoise/css.txt @@ -83,14 +83,10 @@ color: #ff8000; text-decoration: unset } a:active, a:hover, -pre.udiff:focus, -table.sbsdiffcols:focus { - outline: 0 -} abbr[title] { border-bottom: 1px dotted } b, optgroup, @@ -1119,55 +1115,57 @@ } table.login_out .login_out_label { font-weight: 700; text-align: right } -pre.udiff, -table.sbsdiffcols { +table.diff { width: 100%; overflow: auto; padding: 0 5px; font-size: 1rem; background: #000; - border-radius: 5px -} -pre.udiff, -pre.udiff pre, -table.sbsdiffcols pre { - font-size: 1.15rem -} -pre.udiff { + border-radius: 5px; +} +table.diff pre { + font-size: 1.15rem; + scrollbar-color: black #999; +} +table.udiff pre { padding: 10px 0 } -div.difftxtcol { +td.difftxt { width: 52rem; - overflow-x: auto } -span.diffchng { - background-color: #8080e8; - color: #000 -} -span.diffadd { +td.diffln ins { background-color: #559855; - color: #000 + color: #000; + text-decoration: none; +} +td.diffln del { + background-color: #c55; + color: #000; + text-decoration: none; +} +td.difftxt del { + background-color: inherit; + text-decoration: none; } -span.diffrm { +td.difftxt del > del { background-color: #c55; - color: #000 -} -div.diffmkrcol { - padding: 0 1em; - background: #111 -} -span.diffhr { - display: inline-block; - margin: .5em 0 1em; - color: #555 -} -span.diffln { - color: #666 -} + color: #000; + text-decoration: none; +} +td.difftxt ins { + background-color: inherit; + text-decoration: none; +} +td.difftxt ins > ins { + background-color: #559855; + color: #000; + text-decoration: none; +} + table.report { width: 100%; cursor: auto; margin: 0 0 1em; color: #000 Index: skins/blitz/css.txt ================================================================== --- skins/blitz/css.txt +++ skins/blitz/css.txt @@ -1131,19 +1131,19 @@ } /* Diff displays ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––– */ -pre.udiff, table.sbsdiffcols { +table.diff { width: 100%; overflow: auto; border: 1px solid #ccc; padding: 5px; font-size: 1rem; } -pre.udiff:focus, table.sbsdiffcols:focus { +table.diff:focus { outline: none; } /* Ticket Reports Index: skins/darkmode/css.txt ================================================================== --- skins/darkmode/css.txt +++ skins/darkmode/css.txt @@ -451,31 +451,39 @@ } /************************************************************************ diffs... ************************************************************************/ -span.diffchng { - background-color: #8080e8; - color: #000 -} -span.diffadd { +td.diffln ins { background-color: #559855; - color: #000 + color: #000; + text-decoration: none; +} +td.diffln del { + background-color: #c55; + color: #000; + text-decoration: none; +} +td.difftxt del { + background-color: inherit; + text-decoration: none; } -span.diffrm { +td.difftxt del > del { background-color: #c55; - color: #000 -} -div.diffmkrcol { - background: #111 -} -span.diffhr { - color: #555 -} -span.diffln { - color: #666 -} + color: #000; + text-decoration: none; +} +td.difftxt ins { + background-color: inherit; + text-decoration: none; +} +td.difftxt ins > ins { + background-color: #559855; + color: #000; + text-decoration: none; +} + /************************************************************************ ************************************************************************/ body.wikiedit #fossil-status-bar, body.fileedit #fossil-status-bar{ @@ -547,18 +555,14 @@ body.forum .forumPostBody > div blockquote { border: 1px inset; padding: 0 0.5em; } -pre.udiff { - overflow-x: auto; -} - body.report table.report tr td { color: black } body.report table.report a { color: blue } body.tkt td.tktDspValue { color: black } body.tkt td.tktDspValue a { color: blue } body.branch .brlist > table > tbody > tr:hover:not(.selected), body.branch .brlist > table > tbody > tr.selected { background-color: #442800; } Index: skins/default/css.txt ================================================================== --- skins/default/css.txt +++ skins/default/css.txt @@ -158,21 +158,10 @@ padding: 10px; font-size: 0.7em; margin-top: 10px; color: #ccc; } - - -/* Exceptions for /info diff views */ - -.udiff, .sbsdiff { - font-size: .85em !important; - overflow: auto; - border: 1px solid #ccc; - border-radius: 5px; -} - /* Forum */ .forum a:visited { color: #6A7F94; Index: skins/eagle/css.txt ================================================================== --- skins/eagle/css.txt +++ skins/eagle/css.txt @@ -257,11 +257,11 @@ background: rgba(255,255,255,0); } /* Side-by-side diff */ -table.sbsdiff { +table.splitdiff { background-color: #485D7B; font-family: fixed, Dejavu Sans Mono, Monaco, Lucida Console, monospace; font-size: 8pt; border-collapse:collapse; white-space: pre; @@ -320,47 +320,36 @@ margin-top: 3px; line-height: 100%; } /* side-by-side diff display */ -div.sbsdiff { +div.splitdiff { font-family: monospace; font-size: smaller; white-space: pre; } /* context diff display */ -div.udiff { +table.udiff { font-family: monospace; white-space: pre; } -/* changes in a diff */ -span.diffchng { - background-color: rgb(170, 170, 140); -} - /* added code in a diff */ -span.diffadd { +td.difftxt ins > ins, td.diffln ins { background-color: rgb(100, 200, 100); } +td.difftxt ins { + background-color: inherit; +} /* deleted in a diff */ -span.diffrm { +td.difftxt del > del, td.diffln del { background-color: rgb(230, 110, 110); } - -/* suppressed lines in a diff */ -span.diffhr { - display: inline-block; - margin: .5em 0 1em; - color: rgb(150, 150, 140); -} - -/* line numbers in a diff */ -span.diffln { - color: white; +td.difftxt del { + background-color: inherit; } .fileage tr:hover { background-color: #7EA2D9; } @@ -419,15 +408,10 @@ .timelineModernCell[id], .timelineColumnarCell[id], .timelineDetailCell[id] { background-color: #455978; } -div.difflncol { - padding-right: 1em; - text-align: right; - color: white; -} .capsumOff { background-color: #bbbbbb; } .capsumRead { background-color: #006d00; Index: skins/xekri/css.txt ================================================================== --- skins/xekri/css.txt +++ skins/xekri/css.txt @@ -265,91 +265,81 @@ /************************************** * Diffs */ /* Code Added */ -span.diffadd { +td.diffln ins, +td.difftxt ins > ins { background-color: #7f7; color: #000; } - -/* Code Changed */ -span.diffchng { - background-color: #77f; - color: #000; +td.difftxt ins { + background-color: inherit; } /* Code Deleted */ -span.diffrm { +td.diffln del, +td.difftxt del > del { background-color: #f77; color: #000; } +td.difftxt del { + background-color: inherit; +} /************************************** * Diffs : Side-By-Side */ /* display (column-based) */ -table.sbsdiffcols { +table.splitdiff { border-spacing: 0; font-size: 0.85rem; width: 90%; } -table.sbsdiffcols pre { +table.splitdiff pre { border: 0; - margin: 0; + margin: 0 0.5em; padding: 0; } -table.sbsdiffcols td { +table.splitdiff td { padding: 0; vertical-align: top; } /* line number column */ -div.difflncol { +td.diffln { color: #ee0; padding-right: 0.75em; text-align: right; } /* diff text column */ -div.difftxtcol { +td.difftxt { background-color: #111; overflow-x: auto; width: 45em; } -/* suppressed lines */ -span.diffhr { - display: inline-block; - margin-bottom: 0.75em; - color: #ff0; -} /* diff marker column */ -div.diffmkrcol { +td.diffsep { padding: 0 0.5em; } /************************************** * Diffs : Unified */ -pre.udiff { +table.udiff pre { background-color: #111; } -/* line numbers */ -span.diffln { - background-color: #222; - color: #ee0; -} - /************************************** * File List : Flat */ Index: src/blob.c ================================================================== --- src/blob.c +++ src/blob.c @@ -347,10 +347,61 @@ void blob_copy(Blob *pTo, Blob *pFrom){ blob_is_init(pFrom); blob_zero(pTo); blob_append(pTo, blob_buffer(pFrom), blob_size(pFrom)); } + +/* +** Append the second blob onto the end of the first blob and reset the +** second blob. +*/ +void blob_append_xfer(Blob *pTo, Blob *pFrom){ + blob_append(pTo, blob_buffer(pFrom), blob_size(pFrom)); + blob_reset(pFrom); +} + +/* +** Write into pOut, a string literal representation for the first n bytes +** of z[]. The string literal representation is compatible with C, TCL, +** and JSON. Double-quotes are added to both ends. Double-quote and +** backslash characters are escaped. +*/ +void blob_append_tcl_literal(Blob *pOut, const char *z, int n){ + int i; + blob_append_char(pOut, '"'); + for(i=0; i del { + background-color: #ffc0c0; + text-decoration: none; + font-weight: bold; +} +td.difftxt del > del.edit { + background-color: #c0c0ff; + text-decoration: none; + font-weight: bold; +} +td.difftxt ins { + background-color: #dafbe1; + text-decoration: none; +} +td.difftxt ins > ins { + background-color: #a0e4b2; + text-decoration: none; + font-weight: bold; +} +td.difftxt ins > ins.edit { background-color: #c0c0ff; -} -span.diffadd { - background-color: #c0ffc0; -} -span.diffrm { - background-color: #ffc8c8; -} -span.diffhr { - display: inline-block; - margin: .5em 0 1em; - color: #0000ff; -} -span.diffln { - color: #a0a0a0; -} + text-decoration: none; + font-weight: bold; +} + + span.modpending { color: #b03800; font-style: italic; } pre.th1result { Index: src/diff.c ================================================================== --- src/diff.c +++ src/diff.c @@ -42,12 +42,16 @@ #define DIFF_INVERT (((u64)0x02)<<32) /* Invert the diff (debug) */ #define DIFF_CONTEXT_EX (((u64)0x04)<<32) /* Use context even if zero */ #define DIFF_NOTTOOBIG (((u64)0x08)<<32) /* Only display if not too big */ #define DIFF_STRIP_EOLCR (((u64)0x10)<<32) /* Strip trailing CR */ #define DIFF_SLOW_SBS (((u64)0x20)<<32) /* Better but slower side-by-side */ -#define DIFF_WEBPAGE (((u64)0x40)<<32) /* Complete webpage */ -#define DIFF_BROWSER (((u64)0x80)<<32) /* The --browser option */ +#define DIFF_WEBPAGE (((u64)0x00040)<<32) /* Complete webpage */ +#define DIFF_BROWSER (((u64)0x00080)<<32) /* The --browser option */ +#define DIFF_JSON (((u64)0x00100)<<32) /* JSON output */ +#define DIFF_DEBUG (((u64)0x00200)<<32) /* Debugging diff output */ +#define DIFF_RAW (((u64)0x00400)<<32) /* Raw triples - for debugging */ +#define DIFF_TCL (((u64)0x00800)<<32) /* For the --tk option */ /* ** These error messages are shared in multiple locations. They are defined ** here for consistency. */ @@ -95,10 +99,15 @@ /* ** Length of a dline */ #define LENGTH(X) ((X)->n) +/* +** Number of diff chunks generated +*/ +static int nChunk = 0; + /* ** A context for running a raw diff. ** ** The aEdit[] array describes the raw diff. Each triple of integers in ** aEdit[] means: @@ -273,13 +282,13 @@ /* ** Return true if the regular expression *pRe matches any of the ** N dlines */ static int re_dline_match( - ReCompiled *pRe, /* The regular expression to be matched */ - DLine *aDLine, /* First of N DLines to compare against */ - int N /* Number of DLines to check */ + ReCompiled *pRe, /* The regular expression to be matched */ + const DLine *aDLine, /* First of N DLines to compare against */ + int N /* Number of DLines to check */ ){ while( N-- ){ if( re_match(pRe, (const unsigned char *)aDLine->z, LENGTH(aDLine)) ){ return 1; } @@ -292,41 +301,23 @@ ** Append a single line of context-diff output to pOut. */ static void appendDiffLine( Blob *pOut, /* Where to write the line of output */ char cPrefix, /* One of " ", "+", or "-" */ - DLine *pLine, /* The line to be output */ - int html, /* True if generating HTML. False for plain text */ - ReCompiled *pRe /* Colorize only if line matches this Regex */ + DLine *pLine /* The line to be output */ ){ - blob_append(pOut, &cPrefix, 1); - if( html ){ - if( pRe && re_dline_match(pRe, pLine, 1)==0 ){ - cPrefix = ' '; - }else if( cPrefix=='+' ){ - blob_append(pOut, "", -1); - }else if( cPrefix=='-' ){ - blob_append(pOut, "", -1); - } - htmlize_to_blob(pOut, pLine->z, pLine->n); - if( cPrefix!=' ' ){ - blob_append(pOut, "", -1); - } - }else{ - blob_append(pOut, pLine->z, pLine->n); - } - blob_append(pOut, "\n", 1); + blob_append_char(pOut, cPrefix); + blob_append(pOut, pLine->z, pLine->n); + blob_append_char(pOut, '\n'); } /* ** Add two line numbers to the beginning of an output line for a context ** diff. One or the other of the two numbers might be zero, which means -** to leave that number field blank. The "html" parameter means to format -** the output for HTML. +** to leave that number field blank. */ -static void appendDiffLineno(Blob *pOut, int lnA, int lnB, int html){ - if( html ) blob_append(pOut, "", -1); +static void appendDiffLineno(Blob *pOut, int lnA, int lnB){ if( lnA>0 ){ blob_appendf(pOut, "%6d ", lnA); }else{ blob_append(pOut, " ", 7); } @@ -333,21 +324,18 @@ if( lnB>0 ){ blob_appendf(pOut, "%6d ", lnB); }else{ blob_append(pOut, " ", 8); } - if( html ) blob_append(pOut, "", -1); } /* -** Given a raw diff p[] in which the p->aEdit[] array has been filled -** in, compute a context diff into pOut. +** Output a patch-style text diff. */ static void contextDiff( DContext *p, /* The difference */ Blob *pOut, /* Output a context diff to here */ - ReCompiled *pRe, /* Only show changes that match this regex */ u64 diffFlags /* Flags controlling the diff format */ ){ DLine *A; /* Left side of the diff */ DLine *B; /* Right side of the diff */ int a = 0; /* Index of next line in A[] */ @@ -361,17 +349,14 @@ int m; /* Number of lines to output */ int skip; /* Number of lines to skip */ static int nChunk = 0; /* Number of diff chunks seen so far */ int nContext; /* Number of lines of context */ int showLn; /* Show line numbers */ - int html; /* Render as HTML */ int showDivider = 0; /* True to show the divider between diff blocks */ nContext = diff_context_lines(diffFlags); showLn = (diffFlags & DIFF_LINENO)!=0; - html = (diffFlags & DIFF_HTML)!=0; - if( html ) blob_append(pOut, "
\n", -1);
   A = p->aFrom;
   B = p->aTo;
   R = p->aEdit;
   mxr = p->nEdit;
   while( mxr>2 && R[mxr-1]==0 && R[mxr-2]==0 ){ mxr -= 3; }
@@ -378,35 +363,10 @@
   for(r=0; r0 && R[r+nr*3]nContext ){
       na = nb = nContext;
@@ -438,60 +398,55 @@
     nChunk++;
     if( showLn ){
       if( !showDivider ){
         /* Do not show a top divider */
         showDivider = 1;
-      }else if( html ){
-        blob_appendf(pOut, "%.80c\n", '.');
       }else{
         blob_appendf(pOut, "%.80c\n", '.');
       }
-      if( html ) blob_appendf(pOut, "", nChunk);
     }else{
-      if( html ) blob_appendf(pOut, "");
       /*
        * If the patch changes an empty file or results in an empty file,
        * the block header must use 0,0 as position indicator and not 1,0.
        * Otherwise, patch would be confused and may reject the diff.
        */
       blob_appendf(pOut,"@@ -%d,%d +%d,%d @@",
         na ? a+skip+1 : a+skip, na,
         nb ? b+skip+1 : b+skip, nb);
-      if( html ) blob_appendf(pOut, "");
       blob_append(pOut, "\n", 1);
     }
 
     /* Show the initial common area */
     a += skip;
     b += skip;
     m = R[r] - skip;
     for(j=0; jnContext ) m = nContext;
     for(j=0; j\n", -1);
-}
-
-/*
-** Status of a single output line
-*/
-typedef struct SbsLine SbsLine;
-struct SbsLine {
-  Blob *apCols[5];         /* Array of pointers to output columns */
-  int width;               /* Maximum width of a column in the output */
-  unsigned char escHtml;   /* True to escape html characters */
-  int iStart;              /* Write zStart prior to character iStart */
-  const char *zStart;      /* A  tag */
-  int iEnd;                /* Write  prior to character iEnd */
-  int iStart2;             /* Write zStart2 prior to character iStart2 */
-  const char *zStart2;     /* A  tag */
-  int iEnd2;               /* Write  prior to character iEnd2 */
-  ReCompiled *pRe;         /* Only colorize matching lines, if not NULL */
-};
-
-/*
-** Column indices for SbsLine.apCols[]
-*/
-#define SBS_LNA  0     /* Left line number */
-#define SBS_TXTA 1     /* Left text */
-#define SBS_MKR  2     /* Middle separator column */
-#define SBS_LNB  3     /* Right line number */
-#define SBS_TXTB 4     /* Right text */
-
-/*
-** Append newlines to all columns.
-*/
-static void sbsWriteNewlines(SbsLine *p){
-  int i;
-  for( i=p->escHtml ? SBS_LNA : SBS_TXTB; i<=SBS_TXTB; i++ ){
-    blob_append(p->apCols[i], "\n", 1);
-  }
-}
-
-/*
-** Append n spaces to the column.
-*/
-static void sbsWriteSpace(SbsLine *p, int n, int col){
-  blob_appendf(p->apCols[col], "%*s", n, "");
-}
-
-/*
-** Write the text of pLine into column iCol of p.
-**
-** If outputting HTML, write the full line.  Otherwise, only write the
-** width characters.  Translate tabs into spaces.  Add newlines if col
-** is SBS_TXTB.  Translate HTML characters if escHtml is true.  Pad the
-** rendering to width bytes if col is SBS_TXTA and escHtml is false.
-**
-** This comment contains multibyte unicode characters (ü, Æ, ð) in order
-** to test the ability of the diff code to handle such characters.
-*/
-static void sbsWriteText(SbsLine *p, DLine *pLine, int col){
-  Blob *pCol = p->apCols[col];
-  int n = pLine->n;
-  int i;   /* Number of input characters consumed */
-  int k;   /* Cursor position */
-  int needEndSpan = 0;
-  const char *zIn = pLine->z;
-  int w = p->width;
-  int colorize = p->escHtml;
-  if( colorize && p->pRe && re_dline_match(p->pRe, pLine, 1)==0 ){
-    colorize = 0;
-  }
-  for(i=k=0; (p->escHtml || kiStart ){
-        int x = strlen(p->zStart);
-        blob_append(pCol, p->zStart, x);
-        needEndSpan = 1;
-        if( p->iStart2 ){
-          p->iStart = p->iStart2;
-          p->zStart = p->zStart2;
-          p->iStart2 = 0;
-        }
-      }else if( i==p->iEnd ){
-        blob_append(pCol, "", 7);
-        needEndSpan = 0;
-        if( p->iEnd2 ){
-          p->iEnd = p->iEnd2;
-          p->iEnd2 = 0;
-        }
-      }
-    }
-    if( c=='\t' && !p->escHtml ){
-      blob_append(pCol, " ", 1);
-      while( (k&7)!=7 && (p->escHtml || kescHtml ){
-      blob_append(pCol, "<", 4);
-    }else if( c=='&' && p->escHtml ){
-      blob_append(pCol, "&", 5);
-    }else if( c=='>' && p->escHtml ){
-      blob_append(pCol, ">", 4);
-    }else if( c=='"' && p->escHtml ){
-      blob_append(pCol, """, 6);
-    }else{
-      blob_append(pCol, &zIn[i], 1);
-      if( (c&0xc0)==0x80 ) k--;
-    }
-  }
-  if( needEndSpan ){
-    blob_append(pCol, "", 7);
-  }
-  if( col==SBS_TXTB ){
-    sbsWriteNewlines(p);
-  }else if( !p->escHtml ){
-    sbsWriteSpace(p, w-k, SBS_TXTA);
-  }
-}
-
-/*
-** Append a column to the final output blob.
-*/
-static void sbsWriteColumn(Blob *pOut, Blob *pCol, int col){
-  blob_appendf(pOut,
-    "
\n" - "
\n"
-    "%s"
-    "
\n" - "
\n", - (col % 3) ? (col == SBS_MKR ? "mkr" : "txt") : "ln", - blob_str(pCol) - ); -} - -/* -** Append a separator line to column iCol -*/ -static void sbsWriteSep(SbsLine *p, int len, int col){ - char ch = '.'; - if( len<1 ){ - len = 1; - ch = ' '; - } - blob_appendf(p->apCols[col], "%.*c\n", len, ch); -} - -/* -** Append the appropriate marker into the center column of the diff. -*/ -static void sbsWriteMarker(SbsLine *p, const char *zTxt, const char *zHtml){ - blob_append(p->apCols[SBS_MKR], p->escHtml ? zHtml : zTxt, -1); -} - -/* -** Append a line number to the column. -*/ -static void sbsWriteLineno(SbsLine *p, int ln, int col){ - if( p->escHtml ){ - blob_appendf(p->apCols[col], "%d", ln+1); - }else{ - char zLn[7]; - sqlite3_snprintf(7, zLn, "%5d ", ln+1); - blob_appendf(p->apCols[col], "%s ", zLn); - } -} + if( showLn ) appendDiffLineno(pOut, a+j+1, b+j+1); + appendDiffLine(pOut, ' ', &A[a+j]); + } + } +} + +#define MX_CSN 8 /* Maximum number of change spans across a change region */ + +/* +** A description of zero or more (up to MX_CSN) areas of difference +** between two lines of text. +*/ +typedef struct LineChange LineChange; +struct LineChange { + int n; /* Number of change spans */ + struct Span { + int iStart1; /* Byte offset to start of a change on the left */ + int iLen1; /* Length of the left change in bytes */ + int iStart2; /* Byte offset to start of a change on the right */ + int iLen2; /* Length of the change on the right in bytes */ + int isMin; /* True if this change is known to have no useful subdivs */ + } a[MX_CSN]; /* Array of change spans, sorted order */ +}; /* ** The two text segments zLeft and zRight are known to be different on ** both ends, but they might have a common segment in the middle. If ** they do not have a common segment, return 0. If they do have a large @@ -692,123 +499,177 @@ const char *zRight, int nB, /* String on the right */ int *aLCS /* Identify bounds of LCS here */ ){ const unsigned char *zA = (const unsigned char*)zLeft; /* left string */ const unsigned char *zB = (const unsigned char*)zRight; /* right string */ - int nt; /* Number of target points */ - int ti[3]; /* Index for start of each 4-byte target */ - unsigned int target[3]; /* 4-byte alignment targets */ - unsigned int probe; /* probe to compare against target */ - int iAS, iAE, iBS, iBE; /* Range of common segment */ - int i, j; /* Loop counters */ - int rc = 0; /* Result code. 1 for success */ - - if( nA<6 || nB<6 ) return 0; - memset(aLCS, 0, sizeof(int)*4); - ti[0] = i = nB/2-2; - target[0] = (zB[i]<<24) | (zB[i+1]<<16) | (zB[i+2]<<8) | zB[i+3]; - probe = 0; - if( nB<16 ){ - nt = 1; - }else{ - ti[1] = i = nB/4-2; - target[1] = (zB[i]<<24) | (zB[i+1]<<16) | (zB[i+2]<<8) | zB[i+3]; - ti[2] = i = (nB*3)/4-2; - target[2] = (zB[i]<<24) | (zB[i+1]<<16) | (zB[i+2]<<8) | zB[i+3]; - nt = 3; - } - probe = (zA[0]<<16) | (zA[1]<<8) | zA[2]; - for(i=3; i0 && iBS>0 && zA[iAS-1]==zB[iBS-1] ){ iAS--; iBS--; } - if( iAE-iAS > aLCS[1] - aLCS[0] ){ - aLCS[0] = iAS; - aLCS[1] = iAE; - aLCS[2] = iBS; - aLCS[3] = iBE; - rc = 1; - } - } - } - } - return rc; -} - -/* -** Try to shift iStart as far as possible to the left. -*/ -static void sbsShiftLeft(SbsLine *p, const char *z){ - int i, j; - while( (i=p->iStart)>0 && z[i-1]==z[i] ){ - for(j=i+1; jiEnd && z[j-1]==z[j]; j++){} - if( jiEnd ) break; - p->iStart--; - p->iEnd--; - } -} - -/* -** Simplify iStart and iStart2: -** -** * If iStart is a null-change then move iStart2 into iStart -** * Make sure any null-changes are in canonoical form. -** * Make sure all changes are at character boundaries for -** multi-byte characters. -*/ -static void sbsSimplifyLine(SbsLine *p, const char *z){ - if( p->iStart2==p->iEnd2 ){ - p->iStart2 = p->iEnd2 = 0; - }else if( p->iStart2 ){ - while( p->iStart2>0 && (z[p->iStart2]&0xc0)==0x80 ) p->iStart2--; - while( (z[p->iEnd2]&0xc0)==0x80 ) p->iEnd2++; - } - if( p->iStart==p->iEnd ){ - p->iStart = p->iStart2; - p->iEnd = p->iEnd2; - p->zStart = p->zStart2; - p->iStart2 = 0; - p->iEnd2 = 0; - } - if( p->iStart==p->iEnd ){ - p->iStart = p->iEnd = -1; - }else if( p->iStart>0 ){ - while( p->iStart>0 && (z[p->iStart]&0xc0)==0x80 ) p->iStart--; - while( (z[p->iEnd]&0xc0)==0x80 ) p->iEnd++; - } -} - -/* -** Write out lines that have been edited. Adjust the highlight to cover -** only those parts of the line that actually changed. -*/ -static void sbsWriteLineChange( - SbsLine *p, /* The SBS output line */ - DLine *pLeft, /* Left line of the change */ - int lnLeft, /* Line number for the left line */ - DLine *pRight, /* Right line of the change */ - int lnRight /* Line number of the right line */ + int i, j, k; /* Loop counters */ + int lenBest = 0; /* Match length to beat */ + + for(i=0; ilenBest ){ + lenBest = k; + aLCS[0] = i; + aLCS[1] = i+k; + aLCS[2] = j; + aLCS[3] = j+k; + } + } + } + } + return lenBest>0; +} + +/* +** Find the smallest spans that are different between two text strings that +** are known to be different on both ends. +*/ +static int textLineChanges( + const char *zLeft, int nA, /* String on the left */ + const char *zRight, int nB, /* String on the right */ + LineChange *p /* Write results here */ +){ + p->n = 1; + p->a[0].iStart1 = 0; + p->a[0].iLen1 = nA; + p->a[0].iStart2 = 0; + p->a[0].iLen2 = nB; + p->a[0].isMin = 0; + while( p->nn; i++){ + if( p->a[i].isMin ) continue; + x = p->a[i].iLen1; + if( p->a[i].iLen2a[i].iLen2; + if( x>mxLen ){ + mxLen = x; + mxi = i; + } + } + if( mxLen<6 ) break; + x = textLCS(zLeft + p->a[mxi].iStart1, p->a[mxi].iLen1, + zRight + p->a[mxi].iStart2, p->a[mxi].iLen2, aLCS); + if( x==0 ){ + p->a[mxi].isMin = 1; + continue; + } + a = p->a+mxi; + b = a+1; + if( mxin-1 ){ + memmove(b+1, b, sizeof(*b)*(p->n-mxi-1)); + } + p->n++; + b->iStart1 = a->iStart1 + aLCS[1]; + b->iLen1 = a->iLen1 - aLCS[1]; + a->iLen1 = aLCS[0]; + b->iStart2 = a->iStart2 + aLCS[3]; + b->iLen2 = a->iLen2 - aLCS[3]; + a->iLen2 = aLCS[2]; + b->isMin = 0; + } + return p->n; +} + +/* +** Return true if the string starts with n spaces +*/ +static int allSpaces(const char *z, int n){ + int i; + for(i=0; in<1 ) return; + + /* (1) Attempt to move indentation changes to the left margin */ + if( p->a[0].iLen1==0 + && (len = p->a[0].iLen2)>0 + && (j = p->a[0].iStart2)>0 + && zB[0]==zB[j] + && allSpaces(zB, j) + ){ + for(n=1; na[1], &p->a[0], sizeof(p->a[0])*p->n); + p->n++; + p->a[0] = p->a[1]; + p->a[1].iStart2 += n; + p->a[1].iLen2 -= n; + p->a[0].iLen2 = n; + } + p->a[0].iStart1 = 0; + p->a[0].iStart2 = 0; + }else + if( p->a[0].iLen2==0 + && (len = p->a[0].iLen1)>0 + && (j = p->a[0].iStart1)>0 + && zA[0]==zA[j] + && allSpaces(zA, j) + ){ + for(n=1; na[1], &p->a[0], sizeof(p->a[0])*p->n); + p->n++; + p->a[0] = p->a[1]; + p->a[1].iStart1 += n; + p->a[1].iLen1 -= n; + p->a[0].iLen1 = n; + } + p->a[0].iStart1 = 0; + p->a[0].iStart2 = 0; + } + + /* (2) Try to shift changes so that they begin or end with a + ** space. (TBD) */ +} + + +/* +** Given two lines of text, pFrom and pTo, compute a set of changes +** between those two lines, for enhanced display purposes. +** +** The result is written into the LineChange object given by the +** third parameter. +*/ +static void oneLineChange( + const DLine *pLeft, /* Left line of the change */ + const DLine *pRight, /* Right line of the change */ + LineChange *p /* OUTPUT: Write the results here */ ){ int nLeft; /* Length of left line in bytes */ int nRight; /* Length of right line in bytes */ int nShort; /* Shortest of left and right */ int nPrefix; /* Length of common prefix */ int nSuffix; /* Length of common suffix */ + int nCommon; /* Total byte length of suffix and prefix */ const char *zLeft; /* Text of the left line */ const char *zRight; /* Text of the right line */ int nLeftDiff; /* nLeft - nPrefix - nSuffix */ int nRightDiff; /* nRight - nPrefix - nSuffix */ - int aLCS[4]; /* Bounds of common middle segment */ - static const char zClassRm[] = ""; - static const char zClassAdd[] = ""; - static const char zClassChng[] = ""; nLeft = pLeft->n; zLeft = pLeft->z; nRight = pRight->n; zRight = pRight->z; @@ -821,25 +682,27 @@ if( nPrefix0 && (zLeft[nPrefix]&0xc0)==0x80 ) nPrefix--; } nSuffix = 0; if( nPrefix0 && (zLeft[nLeft-nSuffix]&0xc0)==0x80 ) nSuffix--; } if( nSuffix==nLeft || nSuffix==nRight ) nPrefix = 0; } + nCommon = nPrefix + nSuffix; /* If the prefix and suffix overlap, that means that we are dealing with ** a pure insertion or deletion of text that can have multiple alignments. ** Try to find an alignment to begins and ends on whitespace, or on ** punctuation, rather than in the middle of a name or number. */ - if( nPrefix+nSuffix > nShort ){ + if( nCommon > nShort ){ int iBest = -1; int iBestVal = -1; int i; int nLong = nLeftiStart2 = p->iEnd2 = 0; - p->iStart = p->iEnd = -1; - sbsWriteText(p, pLeft, SBS_TXTA); - if( nLeft==nRight && zLeft[nLeft]==zRight[nRight] ){ - sbsWriteMarker(p, " ", ""); - }else{ - sbsWriteMarker(p, " | ", "|"); - } - sbsWriteLineno(p, lnRight, SBS_LNB); - p->iStart = nPrefix; - p->iEnd = nRight - nSuffix; - p->zStart = zClassAdd; - sbsWriteText(p, pRight, SBS_TXTB); + nCommon = nPrefix + nSuffix; + } + + /* A single chunk of text inserted */ + if( nCommon==nLeft ){ + p->n = 1; + p->a[0].iStart1 = nPrefix; + p->a[0].iLen1 = 0; + p->a[0].iStart2 = nPrefix; + p->a[0].iLen2 = nRight - nCommon; + improveReadability(zLeft, zRight, p); return; } - /* A single chunk of text deleted from the left */ - if( nPrefix+nSuffix==nRight ){ - /* Text deleted from the left */ - sbsWriteLineno(p, lnLeft, SBS_LNA); - p->iStart2 = p->iEnd2 = 0; - p->iStart = nPrefix; - p->iEnd = nLeft - nSuffix; - p->zStart = zClassRm; - sbsWriteText(p, pLeft, SBS_TXTA); - sbsWriteMarker(p, " | ", "|"); - sbsWriteLineno(p, lnRight, SBS_LNB); - p->iStart = p->iEnd = -1; - sbsWriteText(p, pRight, SBS_TXTB); + /* A single chunk of text deleted */ + if( nCommon==nRight ){ + p->n = 1; + p->a[0].iStart1 = nPrefix; + p->a[0].iLen1 = nLeft - nCommon; + p->a[0].iStart2 = nPrefix; + p->a[0].iLen2 = 0; + improveReadability(zLeft, zRight, p); return; } /* At this point we know that there is a chunk of text that has ** changed between the left and the right. Check to see if there ** is a large unchanged section in the middle of that changed block. */ - nLeftDiff = nLeft - nSuffix - nPrefix; - nRightDiff = nRight - nSuffix - nPrefix; - if( p->escHtml - && nLeftDiff >= 6 - && nRightDiff >= 6 - && textLCS(&zLeft[nPrefix], nLeftDiff, &zRight[nPrefix], nRightDiff, aLCS) + nLeftDiff = nLeft - nCommon; + nRightDiff = nRight - nCommon; + if( nLeftDiff >= 4 + && nRightDiff >= 4 + && textLineChanges(&zLeft[nPrefix], nLeftDiff, + &zRight[nPrefix], nRightDiff, p)>1 ){ - sbsWriteLineno(p, lnLeft, SBS_LNA); - p->iStart = nPrefix; - p->iEnd = nPrefix + aLCS[0]; - if( aLCS[2]==0 ){ - sbsShiftLeft(p, pLeft->z); - p->zStart = zClassRm; - }else{ - p->zStart = zClassChng; - } - p->iStart2 = nPrefix + aLCS[1]; - p->iEnd2 = nLeft - nSuffix; - p->zStart2 = aLCS[3]==nRightDiff ? zClassRm : zClassChng; - sbsSimplifyLine(p, zLeft); - sbsWriteText(p, pLeft, SBS_TXTA); - sbsWriteMarker(p, " | ", "|"); - sbsWriteLineno(p, lnRight, SBS_LNB); - p->iStart = nPrefix; - p->iEnd = nPrefix + aLCS[2]; - if( aLCS[0]==0 ){ - sbsShiftLeft(p, pRight->z); - p->zStart = zClassAdd; - }else{ - p->zStart = zClassChng; - } - p->iStart2 = nPrefix + aLCS[3]; - p->iEnd2 = nRight - nSuffix; - p->zStart2 = aLCS[1]==nLeftDiff ? zClassAdd : zClassChng; - sbsSimplifyLine(p, zRight); - sbsWriteText(p, pRight, SBS_TXTB); + int i; + for(i=0; in; i++){ + p->a[i].iStart1 += nPrefix; + p->a[i].iStart2 += nPrefix; + } + improveReadability(zLeft, zRight, p); return; } /* If all else fails, show a single big change between left and right */ - sbsWriteLineno(p, lnLeft, SBS_LNA); - p->iStart2 = p->iEnd2 = 0; - p->iStart = nPrefix; - p->iEnd = nLeft - nSuffix; - p->zStart = zClassChng; - sbsWriteText(p, pLeft, SBS_TXTA); - sbsWriteMarker(p, " | ", "|"); - sbsWriteLineno(p, lnRight, SBS_LNB); - p->iEnd = nRight - nSuffix; - sbsWriteText(p, pRight, SBS_TXTB); + p->n = 1; + p->a[0].iStart1 = nPrefix; + p->a[0].iLen1 = nLeft - nCommon; + p->a[0].iStart2 = nPrefix; + p->a[0].iLen2 = nRight - nCommon; + improveReadability(zLeft, zRight, p); +} + +/* +** COMMAND: test-line-diff +** Usage: %fossil% test-line-diff STRING1 STRING2 +** +** Show the differences between the two strings. Used for testing +** the oneLineChange() routine in the diff logic. +*/ +void test_line_diff(void){ + DLine a, b; + LineChange chng; + int i, j, x; + if( g.argc!=4 ) usage("STRING1 STRING2"); + a.z = g.argv[2]; + a.n = (int)strlen(a.z); + b.z = g.argv[3]; + b.n = (int)strlen(b.z); + oneLineChange(&a, &b, &chng); + fossil_print("left: [%s]\n", a.z); + for(i=x=0; i x ){ + if( (a.z[x]&0xc0)!=0x80 ) fossil_print(" "); + x++; + } + for(j=0; j x ){ + if( (b.z[x]&0xc0)!=0x80 ) fossil_print(" "); + x++; + } + for(j=0; jpOut, "SKIP %d (%d..%d left and %d..%d right)%s\n", + n, p->lnLeft+1, p->lnLeft+n, p->lnRight+1, p->lnRight+n, + isFinal ? " FINAL" : ""); + p->lnLeft += n; + p->lnRight += n; +} +static void dfdebugCommon(DiffBuilder *p, const DLine *pLine){ + p->lnLeft++; + p->lnRight++; + blob_appendf(p->pOut, "COMMON %8u %8u %.*s\n", + p->lnLeft, p->lnRight, (int)pLine->n, pLine->z); +} +static void dfdebugInsert(DiffBuilder *p, const DLine *pLine){ + p->lnRight++; + blob_appendf(p->pOut, "INSERT %8d %.*s\n", + p->lnRight, (int)pLine->n, pLine->z); +} +static void dfdebugDelete(DiffBuilder *p, const DLine *pLine){ + p->lnLeft++; + blob_appendf(p->pOut, "DELETE %8u %.*s\n", + p->lnLeft, (int)pLine->n, pLine->z); +} +static void dfdebugReplace(DiffBuilder *p, const DLine *pX, const DLine *pY){ + p->lnLeft++; + p->lnRight++; + blob_appendf(p->pOut, "REPLACE %8u %.*s\n", + p->lnLeft, (int)pX->n, pX->z); + blob_appendf(p->pOut, " %8u %.*s\n", + p->lnRight, (int)pY->n, pY->z); +} +static void dfdebugEdit(DiffBuilder *p, const DLine *pX, const DLine *pY){ + int i, j; + int x; + LineChange chng; + p->lnLeft++; + p->lnRight++; + blob_appendf(p->pOut, "EDIT %8u %.*s\n", + p->lnLeft, (int)pX->n, pX->z); + oneLineChange(pX, pY, &chng); + for(i=x=0; ipOut, "%*s", 25, ""); } + while( ofst > x ){ + if( (pX->z[x]&0xc0)!=0x80 ) blob_append_char(p->pOut, ' '); + x++; + } + for(j=0; jz[x]&0xc0)!=0x80 ) blob_append_char(p->pOut, '^'); + } + } + } + if( x ) blob_append_char(p->pOut, '\n'); + blob_appendf(p->pOut, " %8u %.*s\n", + p->lnRight, (int)pY->n, pY->z); + for(i=x=0; ipOut, "%*s", 25, ""); } + while( ofst > x ){ + if( (pY->z[x]&0xc0)!=0x80 ) blob_append_char(p->pOut, ' '); + x++; + } + for(j=0; jz[x]&0xc0)!=0x80 ) blob_append_char(p->pOut, '^'); + } + } + } + if( x ) blob_append_char(p->pOut, '\n'); +} +static void dfdebugEnd(DiffBuilder *p){ + blob_appendf(p->pOut, "END with %u lines left and %u lines right\n", + p->lnLeft, p->lnRight); + fossil_free(p); +} +static DiffBuilder *dfdebugNew(Blob *pOut){ + DiffBuilder *p = fossil_malloc(sizeof(*p)); + p->xSkip = dfdebugSkip; + p->xCommon = dfdebugCommon; + p->xInsert = dfdebugInsert; + p->xDelete = dfdebugDelete; + p->xReplace = dfdebugReplace; + p->xEdit = dfdebugEdit; + p->xEnd = dfdebugEnd; + p->lnLeft = p->lnRight = 0; + p->pOut = pOut; + return p; +} + +/************************* DiffBuilderTcl ********************************/ +/* +** This formatter outputs a description of the diff formatted as TCL, for +** use by the --tk option to "diff". See also the "diff.tcl" file. The +** output can be viewed directly using the --tcl option. +** +** There is one line per method call: +** +** SKIP n -- Skip "n" lines of input +** COM string -- "string" is an unchanged context line +** INS string -- "string" is in the right file only +** DEL string -- "string" is in the left file only +** EDIT string .... -- Complex edit between left and right +** +** The EDIT verb will be followed by 3*N or 3*N+1 strings. The triples +** each show: +** +** 1. Common text +** 2. Text from the left side +** 3. Text on the right that replaces (2) from the left +** +** For inserted text (2) will be an empty string. For deleted text, (3) +** will be an empty string. (1) might be empty for the first triple if +** the line begins with an edit. After all triples, there might be one +** additional string which is a common suffix. +*/ +static void dftclSkip(DiffBuilder *p, unsigned int n, int isFinal){ + blob_appendf(p->pOut, "SKIP %u\n", n); +} +static void dftclCommon(DiffBuilder *p, const DLine *pLine){ + blob_appendf(p->pOut, "COM "); + blob_append_tcl_literal(p->pOut, pLine->z, pLine->n); + blob_append_char(p->pOut, '\n'); +} +static void dftclInsert(DiffBuilder *p, const DLine *pLine){ + blob_append(p->pOut, "INS ", -1); + blob_append_tcl_literal(p->pOut, pLine->z, pLine->n); + blob_append_char(p->pOut, '\n'); +} +static void dftclDelete(DiffBuilder *p, const DLine *pLine){ + blob_append(p->pOut, "DEL ", -1); + blob_append_tcl_literal(p->pOut, pLine->z, pLine->n); + blob_append_char(p->pOut, '\n'); +} +static void dftclReplace(DiffBuilder *p, const DLine *pX, const DLine *pY){ + blob_append(p->pOut, "EDIT \"\" ", -1); + blob_append_tcl_literal(p->pOut, pX->z, pX->n); + blob_append_char(p->pOut, ' '); + blob_append_tcl_literal(p->pOut, pY->z, pY->n); + blob_append_char(p->pOut, '\n'); +} +static void dftclEdit(DiffBuilder *p, const DLine *pX, const DLine *pY){ + int i, x; + LineChange chng; + blob_append(p->pOut, "EDIT", 4); + oneLineChange(pX, pY, &chng); + for(i=x=0; ipOut, ' '); + blob_append_tcl_literal(p->pOut, pX->z + x, chng.a[i].iStart1 - x); + x = chng.a[i].iStart1; + blob_append_char(p->pOut, ' '); + blob_append_tcl_literal(p->pOut, pX->z + x, chng.a[i].iLen1); + x += chng.a[i].iLen1; + blob_append_char(p->pOut, ' '); + blob_append_tcl_literal(p->pOut, + pY->z + chng.a[i].iStart2, chng.a[i].iLen2); + } + if( xn ){ + blob_append_char(p->pOut, ' '); + blob_append_tcl_literal(p->pOut, pX->z + x, pX->n - x); + } + blob_append_char(p->pOut, '\n'); +} +static void dftclEnd(DiffBuilder *p){ + fossil_free(p); +} +static DiffBuilder *dftclNew(Blob *pOut){ + DiffBuilder *p = fossil_malloc(sizeof(*p)); + p->xSkip = dftclSkip; + p->xCommon = dftclCommon; + p->xInsert = dftclInsert; + p->xDelete = dftclDelete; + p->xReplace = dftclReplace; + p->xEdit = dftclEdit; + p->xEnd = dftclEnd; + p->pOut = pOut; + return p; +} + +/************************* DiffBuilderJson ********************************/ +/* +** This formatter generates a JSON array that describes the difference. +** +** The Json array consists of integer opcodes with each opcode followed +** by zero or more arguments: +** +** Syntax Mnemonic Description +** ----------- -------- -------------------------- +** 0 END This is the end of the diff +** 1 INTEGER SKIP Skip N lines from both files +** 2 STRING COMMON The line show by STRING is in both files +** 3 STRING INSERT The line STRING is in only the right file +** 4 STRING DELETE The STRING line is in only the left file +** 5 SUBARRAY EDIT One line is different on left and right. +** +** The SUBARRAY is an array of 3*N+1 strings with N>=0. The triples +** represent common-text, left-text, and right-text. The last string +** in SUBARRAY is the common-suffix. Any string can be empty if it does +** not apply. +*/ +static void dfjsonSkip(DiffBuilder *p, unsigned int n, int isFinal){ + blob_appendf(p->pOut, "1,%u,\n", n); +} +static void dfjsonCommon(DiffBuilder *p, const DLine *pLine){ + blob_append(p->pOut, "2,",2); + blob_append_json_literal(p->pOut, pLine->z, (int)pLine->n); + blob_append(p->pOut, ",\n",2); +} +static void dfjsonInsert(DiffBuilder *p, const DLine *pLine){ + blob_append(p->pOut, "3,",2); + blob_append_json_literal(p->pOut, pLine->z, (int)pLine->n); + blob_append(p->pOut, ",\n",2); +} +static void dfjsonDelete(DiffBuilder *p, const DLine *pLine){ + blob_append(p->pOut, "4,",2); + blob_append_json_literal(p->pOut, pLine->z, (int)pLine->n); + blob_append(p->pOut, ",\n",2); +} +static void dfjsonReplace(DiffBuilder *p, const DLine *pX, const DLine *pY){ + blob_append(p->pOut, "5,[\"\",",-1); + blob_append_json_literal(p->pOut, pX->z, (int)pX->n); + blob_append(p->pOut, ",",1); + blob_append_json_literal(p->pOut, pY->z, (int)pY->n); + blob_append(p->pOut, ",\"\"],\n",-1); +} +static void dfjsonEdit(DiffBuilder *p, const DLine *pX, const DLine *pY){ + int i, x; + LineChange chng; + blob_append(p->pOut, "5,[", 3); + oneLineChange(pX, pY, &chng); + for(i=x=0; ipOut, pX->z + x, chng.a[i].iStart1 - x); + x = chng.a[i].iStart1; + blob_append_char(p->pOut, ','); + blob_append_json_literal(p->pOut, pX->z + x, chng.a[i].iLen1); + x += chng.a[i].iLen1; + blob_append_char(p->pOut, ','); + blob_append_json_literal(p->pOut, + pY->z + chng.a[i].iStart2, chng.a[i].iLen2); + } + blob_append_char(p->pOut, ','); + blob_append_json_literal(p->pOut, pX->z + x, pX->n - x); + blob_append(p->pOut, "],\n",3); +} +static void dfjsonEnd(DiffBuilder *p){ + blob_append(p->pOut, "0]", 2); + fossil_free(p); +} +static DiffBuilder *dfjsonNew(Blob *pOut){ + DiffBuilder *p = fossil_malloc(sizeof(*p)); + p->xSkip = dfjsonSkip; + p->xCommon = dfjsonCommon; + p->xInsert = dfjsonInsert; + p->xDelete = dfjsonDelete; + p->xReplace = dfjsonReplace; + p->xEdit = dfjsonEdit; + p->xEnd = dfjsonEnd; + p->lnLeft = p->lnRight = 0; + p->pOut = pOut; + blob_append_char(pOut, '['); + return p; +} + +/************************* DiffBuilderUnified********************************/ +/* This formatter generates a unified diff for HTML. +** +** The result is a with four columns. The four columns hold: +** +** 1. The line numbers for the first file. +** 2. The line numbers for the second file. +** 3. The "diff mark": "+" or "-" or just a space +** 4. Text of the line +** +** Inserted lines are marked with and deleted lines are marked +** with . The whole line is marked this way, not just the part that +** changed. The part that change has an additional nested or . +** The CSS needs to be set up such that a single or gives a +** light background and a nested or gives a darker background. +** Additional attributes (like bold font) might also be added to nested +** and since those are the characters that have actually +** changed. +** +** Accumulator strategy: +** +** * Delete line numbers are output directly to p->pOut +** * Insert line numbers accumulate in p->aCol[0]. +** * Separator marks accumulate in p->aCol[1]. +** * Change text accumulates in p->aCol[2]. +** * Pending insert line numbers go into p->aCol[3]. +** * Pending insert text goes into p->aCol[4]. +** +** eState is 1 if text has an open +*/ +static void dfunifiedFinishDelete(DiffBuilder *p){ + if( p->eState==0 ) return; + blob_append(p->pOut, "", 6); + blob_append(&p->aCol[2], "", 6); + p->eState = 0; +} +static void dfunifiedFinishInsert(DiffBuilder *p){ + unsigned int i; + if( p->nPending==0 ) return; + dfunifiedFinishDelete(p); + + /* Blank lines for delete line numbers for each inserted line */ + for(i=0; inPending; i++) blob_append_char(p->pOut, '\n'); + + /* Insert line numbers */ + blob_append(&p->aCol[0], "", 5); + blob_append_xfer(&p->aCol[0], &p->aCol[3]); + blob_append(&p->aCol[0], "", 6); + + /* "+" marks for the separator on inserted lines */ + for(i=0; inPending; i++) blob_append(&p->aCol[1], "+\n", 2); + + /* Text of the inserted lines */ + blob_append(&p->aCol[2], "", 5); + blob_append_xfer(&p->aCol[2], &p->aCol[4]); + blob_append(&p->aCol[2], "", 6); + + p->nPending = 0; +} +static void dfunifiedFinishRow(DiffBuilder *p){ + dfunifiedFinishDelete(p); + dfunifiedFinishInsert(p); + if( blob_size(&p->aCol[0])==0 ) return; + blob_append(p->pOut, "\n", -1); +} +static void dfunifiedStartRow(DiffBuilder *p){ + if( blob_size(&p->aCol[0])>0 ) return; + blob_appendf(p->pOut,"" + "\n", -1); + p->lnLeft += n; + p->lnRight += n; +} +static void dfunifiedCommon(DiffBuilder *p, const DLine *pLine){ + dfunifiedStartRow(p); + dfunifiedFinishDelete(p); + dfunifiedFinishInsert(p); + p->lnLeft++; + p->lnRight++; + blob_appendf(p->pOut,"%d\n", p->lnLeft); + blob_appendf(&p->aCol[0],"%d\n",p->lnRight); + blob_append_char(&p->aCol[1], '\n'); + htmlize_to_blob(&p->aCol[2], pLine->z, (int)pLine->n); + blob_append_char(&p->aCol[2], '\n'); +} +static void dfunifiedInsert(DiffBuilder *p, const DLine *pLine){ + dfunifiedStartRow(p); + p->lnRight++; + blob_appendf(&p->aCol[3],"%d\n", p->lnRight); + blob_append(&p->aCol[4], "", 5); + htmlize_to_blob(&p->aCol[4], pLine->z, (int)pLine->n); + blob_append(&p->aCol[4], "\n", 7); + p->nPending++; +} +static void dfunifiedDelete(DiffBuilder *p, const DLine *pLine){ + dfunifiedStartRow(p); + dfunifiedFinishInsert(p); + if( p->eState==0 ){ + dfunifiedFinishInsert(p); + blob_append(p->pOut, "", 5); + blob_append(&p->aCol[2], "", 5); + p->eState = 1; + } + p->lnLeft++; + blob_appendf(p->pOut,"%d\n", p->lnLeft); + blob_append_char(&p->aCol[0],'\n'); + blob_append(&p->aCol[1],"-\n",2); + blob_append(&p->aCol[2], "", 5); + htmlize_to_blob(&p->aCol[2], pLine->z, (int)pLine->n); + blob_append(&p->aCol[2], "\n", 7); +} +static void dfunifiedReplace(DiffBuilder *p, const DLine *pX, const DLine *pY){ + dfunifiedStartRow(p); + if( p->eState==0 ){ + dfunifiedFinishInsert(p); + blob_append(p->pOut, "", 5); + blob_append(&p->aCol[2], "", 5); + p->eState = 1; + } + p->lnLeft++; + p->lnRight++; + blob_appendf(p->pOut,"%d\n", p->lnLeft); + blob_append_char(&p->aCol[0], '\n'); + blob_append(&p->aCol[1], "-\n", 2); + + htmlize_to_blob(&p->aCol[2], pX->z, pX->n); + blob_append_char(&p->aCol[2], '\n'); + + blob_appendf(&p->aCol[3],"%d\n", p->lnRight); + + htmlize_to_blob(&p->aCol[4], pY->z, pY->n); + blob_append_char(&p->aCol[4], '\n'); + p->nPending++; +} +static void dfunifiedEdit(DiffBuilder *p, const DLine *pX, const DLine *pY){ + int i; + int x; + LineChange chng; + oneLineChange(pX, pY, &chng); + dfunifiedStartRow(p); + if( p->eState==0 ){ + dfunifiedFinishInsert(p); + blob_append(p->pOut, "", 5); + blob_append(&p->aCol[2], "", 5); + p->eState = 1; + } + p->lnLeft++; + p->lnRight++; + blob_appendf(p->pOut,"%d\n", p->lnLeft); + blob_append_char(&p->aCol[0], '\n'); + blob_append(&p->aCol[1], "-\n", 2); + + for(i=x=0; iaCol[2], pX->z+x, ofst - x); + x = ofst; + blob_append(&p->aCol[2], "", 5); + htmlize_to_blob(&p->aCol[2], pX->z+x, len); + x += len; + blob_append(&p->aCol[2], "", 6); + } + } + htmlize_to_blob(&p->aCol[2], pX->z+x, pX->n - x); + blob_append_char(&p->aCol[2], '\n'); + + blob_appendf(&p->aCol[3],"%d\n", p->lnRight); + for(i=x=0; iaCol[4], pY->z+x, ofst - x); + x = ofst; + blob_append(&p->aCol[4], "", 5); + htmlize_to_blob(&p->aCol[4], pY->z+x, len); + x += len; + blob_append(&p->aCol[4], "", 6); + } + } + htmlize_to_blob(&p->aCol[4], pY->z+x, pY->n - x); + blob_append_char(&p->aCol[4], '\n'); + p->nPending++; +} +static void dfunifiedEnd(DiffBuilder *p){ + dfunifiedFinishRow(p); + blob_append(p->pOut, "
\n", -1);
+  blob_append_xfer(p->pOut, &p->aCol[0]);
+  blob_append(p->pOut, "
\n", -1);
+  blob_append_xfer(p->pOut, &p->aCol[1]);
+  blob_append(p->pOut, "
\n",-1);
+  blob_append_xfer(p->pOut, &p->aCol[2]);
+  blob_append(p->pOut, "
\n", ++nChunk);
+  p->eState = 0;
+  p->nPending = 0;
+}
+static void dfunifiedSkip(DiffBuilder *p, unsigned int n, int isFinal){
+  dfunifiedFinishRow(p);
+  blob_append(p->pOut, "
" + "︙
\n",-1); + fossil_free(p); +} +static DiffBuilder *dfunifiedNew(Blob *pOut){ + DiffBuilder *p = fossil_malloc(sizeof(*p)); + p->xSkip = dfunifiedSkip; + p->xCommon = dfunifiedCommon; + p->xInsert = dfunifiedInsert; + p->xDelete = dfunifiedDelete; + p->xReplace = dfunifiedReplace; + p->xEdit = dfunifiedEdit; + p->xEnd = dfunifiedEnd; + p->lnLeft = p->lnRight = 0; + p->eState = 0; + p->nPending = 0; + p->pOut = pOut; + blob_append(pOut, "\n", -1); + blob_init(&p->aCol[0], 0, 0); + blob_init(&p->aCol[1], 0, 0); + blob_init(&p->aCol[2], 0, 0); + blob_init(&p->aCol[3], 0, 0); + blob_init(&p->aCol[4], 0, 0); + return p; +} + +/************************* DiffBuilderSplit ******************************/ +/* This formatter creates a side-by-side diff in HTML. The output is a +**
with 5 columns: +** +** 1. Line numbers for the first file. +** 2. Text for the first file. +** 3. The difference mark. "<", ">", "|" or blank +** 4. Line numbers for the second file. +** 5. Text for the second file. +** +** The and strategy is the same as for unified diff above. +** In fact, the same CSS can be used for both. +** +** Accumulator strategy: +** +** * Left line numbers are output directly to p->pOut +** * Left text accumulates in p->aCol[0]. +** * Edit marks accumulates in p->aCol[1]. +** * Right line numbers accumulate in p->aCol[2]. +** * Right text accumulates in p->aCol[3]. +** +** eState: +** 0 In common block +** 1 Have on the left +** 2 Have on the right +** 3 Have on left and on the right +*/ +static void dfsplitChangeState(DiffBuilder *p, int newState){ + if( p->eState == newState ) return; + if( (p->eState&1)==0 && (newState & 1)!=0 ){ + blob_append(p->pOut, "", 5); + blob_append(&p->aCol[0], "", 5); + p->eState |= 1; + }else if( (p->eState&1)!=0 && (newState & 1)==0 ){ + blob_append(p->pOut, "", 6); + blob_append(&p->aCol[0], "", 6); + p->eState &= ~1; + } + if( (p->eState&2)==0 && (newState & 2)!=0 ){ + blob_append(&p->aCol[2], "", 5); + blob_append(&p->aCol[3], "", 5); + p->eState |= 2; + }else if( (p->eState&2)!=0 && (newState & 2)==0 ){ + blob_append(&p->aCol[2], "", 6); + blob_append(&p->aCol[3], "", 6); + p->eState &= ~2; + } +} +static void dfsplitFinishRow(DiffBuilder *p){ + if( blob_size(&p->aCol[0])==0 ) return; + dfsplitChangeState(p, 0); + blob_append(p->pOut, "\n", -1); +} +static void dfsplitStartRow(DiffBuilder *p){ + if( blob_size(&p->aCol[0])>0 ) return; + blob_appendf(p->pOut,"" + "" + "" + "" + "\n", -1); + p->lnLeft += n; + p->lnRight += n; +} +static void dfsplitCommon(DiffBuilder *p, const DLine *pLine){ + dfsplitStartRow(p); + dfsplitChangeState(p, 0); + p->lnLeft++; + p->lnRight++; + blob_appendf(p->pOut,"%d\n", p->lnLeft); + htmlize_to_blob(&p->aCol[0], pLine->z, (int)pLine->n); + blob_append_char(&p->aCol[0], '\n'); + blob_append_char(&p->aCol[1], '\n'); + blob_appendf(&p->aCol[2],"%d\n",p->lnRight); + htmlize_to_blob(&p->aCol[3], pLine->z, (int)pLine->n); + blob_append_char(&p->aCol[3], '\n'); +} +static void dfsplitInsert(DiffBuilder *p, const DLine *pLine){ + dfsplitStartRow(p); + dfsplitChangeState(p, 2); + p->lnRight++; + blob_append_char(p->pOut, '\n'); + blob_append_char(&p->aCol[0], '\n'); + blob_append(&p->aCol[1], ">\n", -1); + blob_appendf(&p->aCol[2],"%d\n", p->lnRight); + blob_append(&p->aCol[3], "", 5); + htmlize_to_blob(&p->aCol[3], pLine->z, (int)pLine->n); + blob_append(&p->aCol[3], "\n", 7); +} +static void dfsplitDelete(DiffBuilder *p, const DLine *pLine){ + dfsplitStartRow(p); + dfsplitChangeState(p, 1); + p->lnLeft++; + blob_appendf(p->pOut,"%d\n", p->lnLeft); + blob_append(&p->aCol[0], "", 5); + htmlize_to_blob(&p->aCol[0], pLine->z, (int)pLine->n); + blob_append(&p->aCol[0], "\n", 7); + blob_append(&p->aCol[1], "<\n", -1); + blob_append_char(&p->aCol[2],'\n'); + blob_append_char(&p->aCol[3],'\n'); +} +static void dfsplitReplace(DiffBuilder *p, const DLine *pX, const DLine *pY){ + dfsplitStartRow(p); + dfsplitChangeState(p, 3); + p->lnLeft++; + p->lnRight++; + blob_appendf(p->pOut,"%d\n", p->lnLeft); + htmlize_to_blob(&p->aCol[0], pX->z, pX->n); + blob_append_char(&p->aCol[0], '\n'); + + blob_append(&p->aCol[1], "|\n", 2); + + blob_appendf(&p->aCol[2],"%d\n", p->lnRight); + + htmlize_to_blob(&p->aCol[3], pY->z, pY->n); + blob_append_char(&p->aCol[3], '\n'); +} +static void dfsplitEdit(DiffBuilder *p, const DLine *pX, const DLine *pY){ + int i; + int x; + LineChange chng; + oneLineChange(pX, pY, &chng); + dfsplitStartRow(p); + dfsplitChangeState(p, 3); + p->lnLeft++; + p->lnRight++; + blob_appendf(p->pOut,"%d\n", p->lnLeft); + for(i=x=0; iaCol[0], pX->z+x, ofst - x); + x = ofst; + if( chng.a[i].iLen2 ){ + blob_append(&p->aCol[0], "", -1); + }else{ + blob_append(&p->aCol[0], "", 5); + } + htmlize_to_blob(&p->aCol[0], pX->z+x, len); + x += len; + blob_append(&p->aCol[0], "", 6); + } + } + htmlize_to_blob(&p->aCol[0], pX->z+x, pX->n - x); + blob_append_char(&p->aCol[0], '\n'); + + blob_append(&p->aCol[1], "|\n", 2); + + blob_appendf(&p->aCol[2],"%d\n", p->lnRight); + for(i=x=0; iaCol[3], pY->z+x, ofst - x); + x = ofst; + if( chng.a[i].iLen1 ){ + blob_append(&p->aCol[3], "", -1); + }else{ + blob_append(&p->aCol[3], "", 5); + } + htmlize_to_blob(&p->aCol[3], pY->z+x, len); + x += len; + blob_append(&p->aCol[3], "", 6); + } + } + htmlize_to_blob(&p->aCol[3], pY->z+x, pY->n - x); + blob_append_char(&p->aCol[3], '\n'); +} +static void dfsplitEnd(DiffBuilder *p){ + dfsplitFinishRow(p); + blob_append(p->pOut, "
\n",-1);
+  blob_append_xfer(p->pOut, &p->aCol[0]);
+  blob_append(p->pOut, "
\n", -1);
+  blob_append_xfer(p->pOut, &p->aCol[1]);
+  blob_append(p->pOut, "
\n",-1);
+  blob_append_xfer(p->pOut, &p->aCol[2]);
+  blob_append(p->pOut, "
\n",-1);
+  blob_append_xfer(p->pOut, &p->aCol[3]);
+  blob_append(p->pOut, "
\n", ++nChunk);
+  p->eState = 0;
+}
+static void dfsplitSkip(DiffBuilder *p, unsigned int n, int isFinal){
+  dfsplitFinishRow(p);
+  blob_append(p->pOut, 
+     "
\n",-1); + fossil_free(p); +} +static DiffBuilder *dfsplitNew(Blob *pOut){ + DiffBuilder *p = fossil_malloc(sizeof(*p)); + p->xSkip = dfsplitSkip; + p->xCommon = dfsplitCommon; + p->xInsert = dfsplitInsert; + p->xDelete = dfsplitDelete; + p->xReplace = dfsplitReplace; + p->xEdit = dfsplitEdit; + p->xEnd = dfsplitEnd; + p->lnLeft = p->lnRight = 0; + p->eState = 0; + p->pOut = pOut; + blob_append(pOut, "\n", -1); + blob_init(&p->aCol[0], 0, 0); + blob_init(&p->aCol[1], 0, 0); + blob_init(&p->aCol[2], 0, 0); + blob_init(&p->aCol[3], 0, 0); + blob_init(&p->aCol[4], 0, 0); + return p; +} + +/************************* DiffBuilderSbs ******************************/ +/* This formatter creates a side-by-side diff in text. +*/ +static void dfsbsSkip(DiffBuilder *p, unsigned int n, int isFinal){ + if( (p->lnLeft || p->lnRight) && !isFinal ){ + blob_appendf(p->pOut, "%.*c\n", p->width*2 + 16, '.'); + } + p->lnLeft += n; + p->lnRight += n; +} + +/* +** Append at least iMin characters (not bytes) and at most iMax characters +** from pX onto the into of p. +** +** This comment contains multibyte unicode characters (ü, Æ, ð) in order +** to test the ability of the diff code to handle such characters. +*/ +static void sbs_append_chars(Blob *p, int iMin, int iMax, const DLine *pX){ + int i; + const char *z = pX->z; + for(i=0; in; i++){ + char c = z[i]; + blob_append_char(p, c); + if( (c&0xc0)==0x80 ){ iMin++; iMax++; } + } + while( ilnLeft++; + p->lnRight++; + blob_appendf(p->pOut,"%6u ", p->lnLeft); + sbs_append_chars(p->pOut, p->width, p->width, pLine); + blob_appendf(p->pOut," %6u ", p->lnRight); + sbs_append_chars(p->pOut, 0, p->width, pLine); + blob_append_char(p->pOut, '\n'); +} +static void dfsbsInsert(DiffBuilder *p, const DLine *pLine){ + p->lnRight++; + blob_appendf(p->pOut,"%6s %*s > %6u ", + "", p->width, "", p->lnRight); + sbs_append_chars(p->pOut, 0, p->width, pLine); + blob_append_char(p->pOut, '\n'); +} +static void dfsbsDelete(DiffBuilder *p, const DLine *pLine){ + p->lnLeft++; + blob_appendf(p->pOut,"%6u ", p->lnLeft); + sbs_append_chars(p->pOut, p->width, p->width, pLine); + blob_append(p->pOut," <\n", 3); +} +static void dfsbsEdit(DiffBuilder *p, const DLine *pX, const DLine *pY){ + p->lnLeft++; + p->lnRight++; + blob_appendf(p->pOut,"%6u ", p->lnLeft); + sbs_append_chars(p->pOut, p->width, p->width, pX); + blob_appendf(p->pOut, " | %6u ", p->lnRight); + sbs_append_chars(p->pOut, 0, p->width, pY); + blob_append_char(p->pOut, '\n'); +} +static void dfsbsEnd(DiffBuilder *p){ + fossil_free(p); +} +static DiffBuilder *dfsbsNew(Blob *pOut, u64 diffFlags){ + DiffBuilder *p = fossil_malloc(sizeof(*p)); + p->xSkip = dfsbsSkip; + p->xCommon = dfsbsCommon; + p->xInsert = dfsbsInsert; + p->xDelete = dfsbsDelete; + p->xReplace = dfsbsEdit; + p->xEdit = dfsbsEdit; + p->xEnd = dfsbsEnd; + p->lnLeft = p->lnRight = 0; + p->width = diff_width(diffFlags); + p->pOut = pOut; + return p; +} +/****************************************************************************/ /* ** Return the number between 0 and 100 that is smaller the closer pA and ** pB match. Return 0 for a perfect match. Return 100 if pA and pB are ** completely different. ** @@ -972,11 +1658,11 @@ ** (1) Remove leading and trailing whitespace. ** (2) Truncate both strings to at most 250 characters ** (3) Find the length of the longest common subsequence ** (4) Longer common subsequences yield lower scores. */ -static int match_dline(DLine *pA, DLine *pB){ +static int match_dline(const DLine *pA, const DLine *pB){ const char *zA; /* Left string */ const char *zB; /* right string */ int nA; /* Bytes in zA[] */ int nB; /* Bytes in zB[] */ int avg; /* Average length of A and B */ @@ -1024,10 +1710,32 @@ #endif /* Return the result */ return score; } + +/* +** COMMAND: test-line-match +** Usage: %fossil test-line-match STRING1 STRING2 +** +** Return a score from 0 to 100 that is how similar STRING1 is to +** STRING2. Smaller numbers mean more similar. 0 is an exact match. +** +** This command is used to test to match_dline() function in the +** internal Fossil diff logic. +*/ +void test_dline_match(void){ + DLine a, b; + int x; + if( g.argc!=4 ) usage("STRING1 STRING2"); + a.z = g.argv[2]; + a.n = (int)strlen(a.z); + b.z = g.argv[3]; + b.n = (int)strlen(b.z); + x = match_dline(&a, &b); + fossil_print("%d\n", x); +} /* ** There is a change block in which nLeft lines of text on the left are ** converted into nRight lines of text on the right. This routine computes ** how the lines on the left line up with the lines on the right. @@ -1039,61 +1747,113 @@ ** 1. Delete the next line of pLeft. ** 2. Insert the next line of pRight. ** 3. The next line of pLeft changes into the next line of pRight. ** 4. Delete one line from pLeft and add one line to pRight. ** -** Values larger than three indicate better matches. -** -** The length of the returned array will be just large enough to cause -** all elements of pLeft and pRight to be consumed. +** The length of the returned array will be at most nLeft+nRight bytes. +** If the first bytes is 4, that means we could not compute reasonable +** alignment between the two blocks. ** ** Algorithm: Wagner's minimum edit-distance algorithm, modified by ** adding a cost to each match based on how well the two rows match ** each other. Insertion and deletion costs are 50. Match costs ** are between 0 and 100 where 0 is a perfect match 100 is a complete ** mismatch. */ -static unsigned char *sbsAlignment( - DLine *aLeft, int nLeft, /* Text on the left */ - DLine *aRight, int nRight, /* Text on the right */ - u64 diffFlags /* Flags passed into the original diff */ +static unsigned char *diffBlockAlignment( + const DLine *aLeft, int nLeft, /* Text on the left */ + const DLine *aRight, int nRight, /* Text on the right */ + u64 diffFlags, /* Flags passed into the original diff */ + int *pNResult /* OUTPUT: Bytes of result */ ){ int i, j, k; /* Loop counters */ int *a; /* One row of the Wagner matrix */ int *pToFree; /* Space that needs to be freed */ unsigned char *aM; /* Wagner result matrix */ int nMatch, iMatch; /* Number of matching lines and match score */ - int mnLen; /* MIN(nLeft, nRight) */ - int mxLen; /* MAX(nLeft, nRight) */ - int aBuf[100]; /* Stack space for a[] if nRight not to big */ - - aM = fossil_malloc( (nLeft+1)*(nRight+1) ); - if( nLeft==0 ){ - memset(aM, 2, nRight); - return aM; - } - if( nRight==0 ){ - memset(aM, 1, nLeft); - return aM; - } - - /* This algorithm is O(N**2). So if N is too big, bail out with a - ** simple (but stupid and ugly) result that doesn't take too long. */ - mnLen = nLeft100000 && (diffFlags & DIFF_SLOW_SBS)==0 ){ - memset(aM, 4, mnLen); - if( nLeft>mnLen ) memset(aM+mnLen, 1, nLeft-mnLen); - if( nRight>mnLen ) memset(aM+mnLen, 2, nRight-mnLen); - return aM; - } - + int mnLen; /* minInt(nLeft, nRight) */ + int mxLen; /* MAX(nLeft, nRight) */ + int aBuf[100]; /* Stack space for a[] if nRight not to big */ + + if( nLeft==0 ){ + aM = fossil_malloc( nRight + 2 ); + memset(aM, 2, nRight); + *pNResult = nRight; + return aM; + } + if( nRight==0 ){ + aM = fossil_malloc( nLeft + 2 ); + memset(aM, 1, nLeft); + *pNResult = nLeft; + return aM; + } + + /* For large alignments, use a divide and conquer algorithm that is + ** O(NlogN). The result is not as precise, but this whole thing is an + ** approximation anyhow, and the faster response time is an acceptable + ** trade-off for reduced precision. + */ + mnLen = nLeft1000 && (diffFlags & DIFF_SLOW_SBS)==0 ){ + const DLine *aSmall; /* The smaller of aLeft and aRight */ + const DLine *aBig; /* The larger of aLeft and aRight */ + int nSmall, nBig; /* Size of aSmall and aBig. nSmall<=nBig */ + int iDivSmall, iDivBig; /* Divider point for aSmall and aBig */ + int iDivLeft, iDivRight; /* Divider point for aLeft and aRight */ + unsigned char *a1, *a2; /* Results of the alignments on two halves */ + int n1, n2; /* Number of entries in a1 and a2 */ + int score, bestScore; /* Score and best score seen so far */ + if( nLeft>nRight ){ + aSmall = aRight; + nSmall = nRight; + aBig = aLeft; + nBig = nLeft; + }else{ + aSmall = aLeft; + nSmall = nLeft; + aBig = aRight; + nBig = nRight; + } + iDivBig = nBig/2; + iDivSmall = nSmall/2; + bestScore = 10000; + for(i=0; inRight ? nLeft : nRight; if( i*4>mxLen*5 && (nMatch==0 || iMatch/nMatch>15) ){ - memset(aM, 4, mnLen); - if( nLeft>mnLen ) memset(aM+mnLen, 1, nLeft-mnLen); - if( nRight>mnLen ) memset(aM+mnLen, 2, nRight-mnLen); + memset(aM, 4, mnLen); *pNResult = mnLen; + if( nLeft>mnLen ){ memset(aM+mnLen, 1, nLeft-mnLen); *pNResult = nLeft; } + if( nRight>mnLen ){ memset(aM+mnLen, 2, nRight-mnLen); *pNResult = nRight; } } /* Return the result */ fossil_free(pToFree); return aM; @@ -1177,70 +1938,47 @@ ** R[] is an array of six integer, two COPY/DELETE/INSERT triples for a ** pair of adjacent differences. Return true if the gap between these ** two differences is so small that they should be rendered as a single ** edit. */ -static int smallGap(int *R){ +static int smallGap(const int *R){ return R[3]<=2 || R[3]<=(R[1]+R[2]+R[4]+R[5])/8; } /* -** Given a diff context in which the aEdit[] array has been filled -** in, compute a side-by-side diff into pOut. +** Format a diff using a DiffBuilder object */ -static void sbsDiff( - DContext *p, /* The computed diff */ - Blob *pOut, /* Write the results here */ - ReCompiled *pRe, /* Only show changes that match this regex */ - u64 diffFlags /* Flags controlling the diff */ +static void formatDiff( + DContext *p, /* The computed diff */ + ReCompiled *pRe, /* Only show changes that match this regex */ + u64 diffFlags, /* Flags controlling the diff */ + DiffBuilder *pBuilder /* The formatter object */ ){ - DLine *A; /* Left side of the diff */ - DLine *B; /* Right side of the diff */ - int a = 0; /* Index of next line in A[] */ - int b = 0; /* Index of next line in B[] */ - int *R; /* Array of COPY/DELETE/INSERT triples */ - int r; /* Index into R[] */ - int nr; /* Number of COPY/DELETE/INSERT triples to process */ - int mxr; /* Maximum value for r */ - int na, nb; /* Number of lines shown from A and B */ - int i, j; /* Loop counters */ - int m, ma, mb;/* Number of lines to output */ - int skip; /* Number of lines to skip */ - static int nChunk = 0; /* Number of chunks of diff output seen so far */ - SbsLine s; /* Output line buffer */ - int nContext; /* Lines of context above and below each change */ - int showDivider = 0; /* True to show the divider */ - Blob aCols[5]; /* Array of column blobs */ - - memset(&s, 0, sizeof(s)); - s.width = diff_width(diffFlags); - nContext = diff_context_lines(diffFlags); - s.escHtml = (diffFlags & DIFF_HTML)!=0; - if( s.escHtml ){ - for(i=SBS_LNA; i<=SBS_TXTB; i++){ - blob_zero(&aCols[i]); - s.apCols[i] = &aCols[i]; - } - }else{ - for(i=SBS_LNA; i<=SBS_TXTB; i++){ - s.apCols[i] = pOut; - } - } - s.pRe = pRe; - s.iStart = -1; - s.iStart2 = 0; - s.iEnd = -1; + const DLine *A; /* Left side of the diff */ + const DLine *B; /* Right side of the diff */ + unsigned int a = 0; /* Index of next line in A[] */ + unsigned int b = 0; /* Index of next line in B[] */ + const int *R; /* Array of COPY/DELETE/INSERT triples */ + unsigned int r; /* Index into R[] */ + unsigned int nr; /* Number of COPY/DELETE/INSERT triples to process */ + unsigned int mxr; /* Maximum value for r */ + unsigned int na, nb; /* Number of lines shown from A and B */ + unsigned int i, j; /* Loop counters */ + unsigned int m, ma, mb;/* Number of lines to output */ + signed int skip = 0; /* Number of lines to skip */ + unsigned int nContext; /* Lines of context above and below each change */ + + nContext = diff_context_lines(diffFlags); A = p->aFrom; B = p->aTo; R = p->aEdit; mxr = p->nEdit; while( mxr>2 && R[mxr-1]==0 && R[mxr-2]==0 ){ mxr -= 3; } for(r=0; r0 && R[r+nr*3]nContext ){ na = nb = nContext; skip = R[r] - nContext; }else{ @@ -1289,127 +2027,91 @@ for(i=1; i", nChunk); - } - /* Show the initial common area */ a += skip; b += skip; m = R[r] - skip; + if( r ) skip -= nContext; + if( skip>0 ){ + pBuilder->xSkip(pBuilder, skip, 0); + } for(j=0; jxCommon(pBuilder, &A[a+j]); } a += m; b += m; /* Show the differences */ for(i=0; i0; j++){ - if( alignment[j]==1 ){ - /* Delete one line from the left */ - sbsWriteLineno(&s, a, SBS_LNA); - s.iStart = 0; - s.zStart = ""; - s.iEnd = LENGTH(&A[a]); - sbsWriteText(&s, &A[a], SBS_TXTA); - sbsWriteMarker(&s, " <", "<"); - sbsWriteNewlines(&s); - assert( ma>0 ); - ma--; - a++; - }else if( alignment[j]==3 ){ - /* The left line is changed into the right line */ - sbsWriteLineChange(&s, &A[a], a, &B[b], b); - assert( ma>0 && mb>0 ); - ma--; - mb--; - a++; - b++; - }else if( alignment[j]==2 ){ - /* Insert one line on the right */ - if( !s.escHtml ){ - sbsWriteSpace(&s, s.width + 7, SBS_TXTA); - } - sbsWriteMarker(&s, " > ", ">"); - sbsWriteLineno(&s, b, SBS_LNB); - s.iStart = 0; - s.zStart = ""; - s.iEnd = LENGTH(&B[b]); - sbsWriteText(&s, &B[b], SBS_TXTB); - assert( mb>0 ); - mb--; - b++; - }else{ - /* Delete from the left and insert on the right */ - sbsWriteLineno(&s, a, SBS_LNA); - s.iStart = 0; - s.zStart = ""; - s.iEnd = LENGTH(&A[a]); - sbsWriteText(&s, &A[a], SBS_TXTA); - sbsWriteMarker(&s, " | ", "|"); - sbsWriteLineno(&s, b, SBS_LNB); - s.iStart = 0; - s.zStart = ""; - s.iEnd = LENGTH(&B[b]); - sbsWriteText(&s, &B[b], SBS_TXTB); - ma--; - mb--; - a++; - b++; - } - } + fossil_free(alignment); + alignment = diffBlockAlignment(&A[a], ma, &B[b], mb, diffFlags,&nAlign); + } + + for(j=0; ma+mb>0; j++){ + assert( jxDelete(pBuilder, &A[a]); + ma--; + a++; + break; + } + case 2: { + /* Insert one line on the right */ + pBuilder->xInsert(pBuilder, &B[b]); + assert( mb>0 ); + mb--; + b++; + break; + } + case 3: { + /* The left line is changed into the right line */ + pBuilder->xEdit(pBuilder, &A[a], &B[b]); + assert( ma>0 && mb>0 ); + ma--; + mb--; + a++; + b++; + break; + } + case 4: { + /* Delete from left then separately insert on the right */ + pBuilder->xReplace(pBuilder, &A[a], &B[b]); + ma--; + a++; + mb--; + b++; + break; + } + } + } + assert( nAlign==j ); fossil_free(alignment); if( ixCommon(pBuilder, &A[a+j]); } b += m; a += m; } } @@ -1416,29 +2118,20 @@ /* Show the final common area */ assert( nr==i ); m = R[r+nr*3]; if( m>nContext ) m = nContext; - for(j=0; j0 ){ - blob_append(pOut, "
\n", -1); - for(i=SBS_LNA; i<=SBS_TXTB; i++){ - sbsWriteColumn(pOut, s.apCols[i], i); - blob_reset(s.apCols[i]); - } - blob_append(pOut, "
\n", -1); - } -} + for(j=0; jxCommon(pBuilder, &A[a+j]); + } + } + if( R[r]>nContext ){ + pBuilder->xSkip(pBuilder, R[r] - nContext, 1); + } + pBuilder->xEnd(pBuilder); +} + /* ** Compute the optimal longest common subsequence (LCS) using an ** exhaustive search. This version of the LCS is only used for ** shorter input strings since runtime is O(N*N) where N is the @@ -1509,11 +2202,11 @@ int n; /* Loop limit */ DLine *pA, *pB; /* Pointers to lines */ int iSX, iSY, iEX, iEY; /* Current match */ int skew = 0; /* How lopsided is the match */ int dist = 0; /* Distance of match from center */ - int mid; /* Center of the span */ + int mid; /* Center of the chng */ int iSXb, iSYb, iEXb, iEYb; /* Best match so far */ int iSXp, iSYp, iEXp, iEYp; /* Previous match */ sqlite3_int64 bestScore; /* Best score so far */ sqlite3_int64 score; /* Score for current candidate LCS */ int span; /* combined width of the input sequences */ @@ -1878,18 +2571,26 @@ blob_append(pOut, msg, -1); } } /* -** Generate a report of the differences between files pA and pB. -** If pOut is not NULL then a unified diff is appended there. It -** is assumed that pOut has already been initialized. If pOut is -** NULL, then a pointer to an array of integers is returned. -** The integers come in triples. For each triple, -** the elements are the number of lines copied, the number of -** lines deleted, and the number of lines inserted. The vector -** is terminated by a triple of all zeros. +** Generate a report of the differences between files pA_Blob and pB_Blob. +** +** If pOut!=NULL then append text to pOut that will be the difference, +** formatted according to flags in diffFlags. The pOut Blob must have +** already been initialized. +** +** If pOut==NULL then no formatting occurs. Instead, this routine +** returns a pointer to an array of integers. The integers come in +** triples. The elements of each triple are: +** +** 1. The number of lines to copy +** 2. The number of lines to delete +** 3. The number of lines to insert +** +** The return vector is terminated bin a triple of all zeros. The caller +** should free the returned vector using fossil_free(). ** ** This diff utility does not work on binary files. If a binary ** file is encountered, 0 is returned and pOut is written with ** text "cannot compute difference between binary files". */ @@ -1969,14 +2670,40 @@ g.diffCnt[2] += nDel; if( nIns+nDel ){ g.diffCnt[0]++; blob_appendf(pOut, "%10d %10d", nIns, nDel); } + }else if( diffFlags & DIFF_RAW ){ + const int *R = c.aEdit; + unsigned int r; + for(r=0; R[r] || R[r+1] || R[r+2]; r += 3){ + blob_appendf(pOut, " copy %6d delete %6d insert %6d\n", + R[r], R[r+1], R[r+2]); + } + }else if( diffFlags & DIFF_JSON ){ + DiffBuilder *pBuilder = dfjsonNew(pOut); + formatDiff(&c, pRe, diffFlags, pBuilder); + blob_append_char(pOut, '\n'); + }else if( diffFlags & DIFF_TCL ){ + DiffBuilder *pBuilder = dftclNew(pOut); + formatDiff(&c, pRe, diffFlags, pBuilder); }else if( diffFlags & DIFF_SIDEBYSIDE ){ - sbsDiff(&c, pOut, pRe, diffFlags); + DiffBuilder *pBuilder; + if( diffFlags & DIFF_HTML ){ + pBuilder = dfsplitNew(pOut); + }else{ + pBuilder = dfsbsNew(pOut, diffFlags); + } + formatDiff(&c, pRe, diffFlags, pBuilder); + }else if( diffFlags & DIFF_DEBUG ){ + DiffBuilder *pBuilder = dfdebugNew(pOut); + formatDiff(&c, pRe, diffFlags, pBuilder); + }else if( diffFlags & DIFF_HTML ){ + DiffBuilder *pBuilder = dfunifiedNew(pOut); + formatDiff(&c, pRe, diffFlags, pBuilder); }else{ - contextDiff(&c, pOut, pRe, diffFlags); + contextDiff(&c, pOut, diffFlags); } fossil_free(c.aFrom); fossil_free(c.aTo); fossil_free(c.aEdit); return 0; @@ -2049,48 +2776,40 @@ } if( find_option("by",0,0)!=0 ){ diffFlags |= DIFF_HTML|DIFF_WEBPAGE|DIFF_LINENO|DIFF_BROWSER |DIFF_SIDEBYSIDE; } + if( find_option("json",0,0)!=0 ){ + diffFlags |= DIFF_JSON; + } + if( find_option("tcl",0,0)!=0 ){ + diffFlags |= DIFF_TCL; + } + + /* Undocumented and unsupported flags used for development + ** debugging and analysis: */ + if( find_option("debug",0,0)!=0 ) diffFlags |= DIFF_DEBUG; + if( find_option("raw",0,0)!=0 ) diffFlags |= DIFF_RAW; return diffFlags; } -/* -** COMMAND: test-rawdiff -** -** Usage: %fossil test-rawdiff FILE1 FILE2 -** -** Show a minimal sequence of Copy/Delete/Insert operations needed to convert -** FILE1 into FILE2. This command is intended for use in testing and debugging -** the built-in difference engine of Fossil. -*/ -void test_rawdiff_cmd(void){ - Blob a, b; - int r; - int i; - int *R; - u64 diffFlags = diff_options(); - if( g.argc<4 ) usage("FILE1 FILE2 ..."); - blob_read_from_file(&a, g.argv[2], ExtFILE); - for(i=3; i3 ) fossil_print("-------------------------------\n"); - blob_read_from_file(&b, g.argv[i], ExtFILE); - R = text_diff(&a, &b, 0, 0, diffFlags); - for(r=0; R[r] || R[r+1] || R[r+2]; r += 3){ - fossil_print(" copy %4d delete %4d insert %4d\n", R[r], R[r+1], R[r+2]); - } - /* free(R); */ - blob_reset(&b); - } -} - /* ** COMMAND: test-diff +** COMMAND: xdiff +** +** Usage: %fossil xdiff [options] FILE1 FILE2 +** +** This is the "external diff" feature. By "external" here we mean a diff +** applied to files that are not under version control. See the "diff" +** command for computing differences between files that are under control. ** -** Usage: %fossil [options] FILE1 FILE2 +** This command prints the differences between the two files FILE1 and FILE2. +** all of the usual diff command-line options apply. See the "diff" command +** for a full list of command-line options. ** -** Print the difference between two files. The usual diff options apply. +** This command used to be called "test-diff". The older "test-diff" spelling +** still works, for compatibility. */ void test_diff_cmd(void){ Blob a, b, out; u64 diffFlag; const char *zRe; /* Regex filter for diff output */ ADDED src/diff.js Index: src/diff.js ================================================================== --- /dev/null +++ src/diff.js @@ -0,0 +1,69 @@ +/* Refinements to the display of unified and side-by-side diffs. +** +** In all cases, the table columns tagged with "difftxt" are expanded, +** where possible, to fill the width of the screen. +** +** For a side-by-side diff, if either column is two wide to fit on the +** display, scrollbars are added. The scrollbars are linked, so that +** both sides scroll together. Left and right arrows also scroll. +*/ +(function(){ + var SCROLL_LEN = 25; + function initDiff(diff){ + var txtCols = diff.querySelectorAll('td.difftxt'); + var txtPres = diff.querySelectorAll('td.difftxt pre'); + var width = 0; + if(txtPres.length>=2)Math.max(txtPres[0].scrollWidth, txtPres[1].scrollWidth); + var i; + for(i=0; i]*>(.+)} $line - errMsg]} { - continue - } - incr nDiffs - set idx [expr {$nDiffs > 1 ? [.txtA index end] : "1.0"}] - .wfiles.lb insert end $fn - - foreach c [cols] { - if {$nDiffs > 1} { - $c insert end \n - - } - if {[colType $c] eq "txt"} { - $c insert end $fn\n fn - if {$fn2!=""} {set fn $fn2} - } else { - $c insert end \n fn - } - $c insert end \n - - - if {$errMsg ne ""} continue - while {[getLine $difftxt $N ii] ne "
"} continue
-      set type [colType $c]
-      set str {}
-      while {[set line [getLine $difftxt $N ii]] ne "
"} { - set len [string length [dehtml $line]] - if {$len > $widths($type)} { - set widths($type) $len - } - append str $line\n - } - - set re {([^<]*)} - # Use \r as separator since it can't appear in the diff output (it gets - # converted to a space). - set str [regsub -all $re $str "\r\\1\r\\2\r"] - foreach {pre class mid} [split $str \r] { - if {$class ne ""} { - $c insert end [dehtml $pre] - [dehtml $mid] [list $class -] - } else { - $c insert end [dehtml $pre] - - } - } - } - - if {$errMsg ne ""} { - foreach c {.txtA .txtB} {$c insert end [string trim $errMsg] err} - foreach c [cols] {$c insert end \n -} + set n1 0 + set n2 0 + array set widths {txt 0 ln 0 mkr 1} + while {[set line [getLine $difftxt $N ii]] != -1} { + switch -- [lindex $line 0] { + FILE { + incr nDiffs + foreach wx [list [string length $n1] [string length $n2]] { + if {$wx>$widths(ln)} {set widths(ln) $wx} + } + .lnA insert end \n fn \n - + .txtA insert end [lindex $line 1]\n fn \n - + .mkr insert end \n fn \n - + .lnB insert end \n fn \n - + .txtB insert end [lindex $line 2]\n fn \n - + .wfiles.lb insert end [lindex $line 2] + set n1 0 + set n2 0 + } + SKIP { + set n [lindex $line 1] + incr n1 $n + incr n2 $n + .lnA insert end ...\n hrln + .txtA insert end [string repeat . 30]\n hrtxt + .mkr insert end \n hrln + .lnB insert end ...\n hrln + .txtB insert end [string repeat . 30]\n hrtxt + } + COM { + set x [lindex $line 1] + incr n1 + incr n2 + .lnA insert end $n1\n - + .txtA insert end $x\n - + .mkr insert end \n - + .lnB insert end $n2\n - + .txtB insert end $x\n - + } + INS { + set x [lindex $line 1] + incr n2 + .lnA insert end \n - + .txtA insert end \n - + .mkr insert end >\n - + .lnB insert end $n2\n - + .txtB insert end $x add \n - + } + DEL { + set x [lindex $line 1] + incr n1 + .lnA insert end $n1\n - + .txtA insert end $x rm \n - + .mkr insert end <\n - + .lnB insert end \n - + .txtB insert end \n - + } + EDIT { + incr n1 + incr n2 + .lnA insert end $n1\n - + .lnB insert end $n2\n - + .mkr insert end |\n - + set nn [llength $line] + for {set i 1} {$i<$nn} {incr i 3} { + set x [lindex $line $i] + if {$x ne ""} { + .txtA insert end $x - + .txtB insert end $x - + } + if {$i+2<$nn} { + set x1 [lindex $line [expr {$i+1}]] + set x2 [lindex $line [expr {$i+2}]] + if {"$x1" eq ""} { + .txtB insert end $x2 add + } elseif {"$x2" eq ""} { + .txtA insert end $x1 rm + } else { + .txtA insert end $x1 chng + .txtB insert end $x2 chng + } + } + } + .txtA insert end \n - + .txtB insert end \n - + } + "" { + foreach wx [list [string length $n1] [string length $n2]] { + if {$wx>$widths(ln)} {set widths(ln) $wx} + } + } + default { + error "bad diff source line: $line" + } } } foreach c [cols] { set type [colType $c] @@ -330,12 +366,14 @@ foreach c [cols] { set keyPrefix [string toupper [colType $c]]_COL_ if {[tk windowingsystem] eq "win32"} {$c config -font {courier 9}} $c config -bg $CFG(${keyPrefix}BG) -fg $CFG(${keyPrefix}FG) -borderwidth 0 \ -padx $CFG(PADX) -yscroll sync-y - $c tag config hr -spacing1 $CFG(HR_PAD_TOP) -spacing3 $CFG(HR_PAD_BTM) \ - -foreground $CFG(HR_FG) + $c tag config hrln -spacing1 $CFG(HR_PAD_TOP) -spacing3 $CFG(HR_PAD_BTM) \ + -foreground $CFG(HR_FG) -justify right + $c tag config hrtxt -spacing1 $CFG(HR_PAD_TOP) -spacing3 $CFG(HR_PAD_BTM) \ + -foreground $CFG(HR_FG) -justify center $c tag config fn -spacing1 $CFG(FN_PAD) -spacing3 $CFG(FN_PAD) bindtags $c ". $c Text all" bind $c <1> {focus %W} } Index: src/diffcmd.c ================================================================== --- src/diffcmd.c +++ src/diffcmd.c @@ -114,11 +114,12 @@ /* ** Print the "Index:" message that patches wants to see at the top of a diff. */ void diff_print_index(const char *zFile, u64 diffFlags, Blob *diffBlob){ - if( (diffFlags & (DIFF_SIDEBYSIDE|DIFF_BRIEF|DIFF_NUMSTAT|DIFF_WEBPAGE))==0 ){ + if( (diffFlags & (DIFF_SIDEBYSIDE|DIFF_BRIEF|DIFF_NUMSTAT|DIFF_JSON| + DIFF_WEBPAGE|DIFF_TCL))==0 ){ char *z = mprintf("Index: %s\n%.66c\n", zFile, '='); if( !diffBlob ){ fossil_print("%s", z); }else{ blob_appendf(diffBlob, "%s", z); @@ -128,21 +129,47 @@ } /* ** Print the +++/--- filename lines for a diff operation. */ -void diff_print_filenames(const char *zLeft, const char *zRight, - u64 diffFlags, Blob *diffBlob){ +void diff_print_filenames( + const char *zLeft, + const char *zRight, + u64 diffFlags, + Blob *diffBlob +){ char *z = 0; - if( diffFlags & DIFF_BRIEF ){ + if( diffFlags & (DIFF_BRIEF|DIFF_RAW|DIFF_JSON) ){ /* no-op */ + }else if( diffFlags & DIFF_DEBUG ){ + fossil_print("FILE-LEFT %s\nFILE-RIGHT %s\n", + zLeft, zRight); }else if( diffFlags & DIFF_WEBPAGE ){ if( fossil_strcmp(zLeft,zRight)==0 ){ z = mprintf("

%h

\n", zLeft); }else{ z = mprintf("

%h ⇆ %h

\n", zLeft, zRight); } + }else if( diffFlags & DIFF_TCL ){ + Blob *pOut; + Blob x; + if( diffBlob ){ + pOut = diffBlob; + }else{ + blob_init(&x, 0, 0); + pOut = &x; + } + blob_append(pOut, "FILE ", 5); + blob_append_tcl_literal(pOut, zLeft, (int)strlen(zLeft)); + blob_append_char(pOut, ' '); + blob_append_tcl_literal(pOut, zRight, (int)strlen(zRight)); + blob_append_char(pOut, '\n'); + if( !diffBlob ){ + fossil_print("%s", blob_str(pOut)); + blob_reset(&x); + } + return; }else if( diffFlags & DIFF_SIDEBYSIDE ){ int w = diff_width(diffFlags); int n1 = strlen(zLeft); int n2 = strlen(zRight); int x; @@ -168,41 +195,10 @@ blob_appendf(diffBlob, "%s", z); } fossil_free(z); } -/* -** Extra CSS for side-by-side diffs -*/ -static const char zSbsCss[] = -@ table.sbsdiffcols { -@ width: 90%; -@ border-spacing: 0; -@ font-size: small; -@ } -@ table.sbsdiffcols td { -@ padding: 0; -@ vertical-align: top; -@ } -@ table.sbsdiffcols pre { -@ margin: 0; -@ padding: 0; -@ border: 0; -@ } -@ div.difflncol { -@ padding-right: 1em; -@ text-align: right; -@ color: #a0a0a0; -@ } -@ div.difftxtcol { -@ width: 10em; -@ overflow-x: auto; -@ } -@ div.diffmkrcol { -@ padding: 0 1em; -@ } -; /* ** Default header text for diff with --webpage */ static const char zWebpageHdr[] = @@ -209,30 +205,77 @@ @ @ @ @ @ @ @ ; const char zWebpageEnd[] = @@ -294,17 +337,11 @@ #else SetConsoleCtrlHandler(diff_console_ctrl_handler, TRUE); #endif } if( (diffFlags & DIFF_WEBPAGE)!=0 ){ - const char *zExtra; - if( diffFlags & DIFF_SIDEBYSIDE ){ - zExtra = zSbsCss; - }else{ - zExtra = ""; - } - fossil_print(zWebpageHdr/*works-like:"%s"*/, zExtra); + fossil_print("%s",zWebpageHdr); fflush(stdout); } } /* Do any final output required by a diff and complete the diff @@ -318,11 +355,11 @@ ** of FOSSIL_BROWSER_DIFF_DELAY milliseconds, delete the temp file. */ void diff_end(u64 diffFlags, int nErr){ if( (diffFlags & DIFF_WEBPAGE)!=0 ){ if( diffFlags & DIFF_SIDEBYSIDE ){ - const unsigned char *zJs = builtin_file("sbsdiff.js", 0); + const unsigned char *zJs = builtin_file("diff.js", 0); fossil_print("\n", zJs); } fossil_print("%s", zWebpageEnd); } if( (diffFlags & DIFF_BROWSER)!=0 && nErr==0 ){ @@ -582,12 +619,12 @@ Blob sql; Stmt q; int asNewFile; /* Treat non-existant files as empty files */ int isNumStat; /* True for --numstat */ - asNewFile = (diffFlags & (DIFF_VERBOSE|DIFF_NUMSTAT))!=0; - isNumStat = (diffFlags & DIFF_NUMSTAT)!=0; + asNewFile = (diffFlags & (DIFF_VERBOSE|DIFF_NUMSTAT|DIFF_HTML))!=0; + isNumStat = (diffFlags & (DIFF_NUMSTAT|DIFF_TCL|DIFF_HTML))!=0; vid = db_lget_int("checkout", 0); vfile_check_signature(vid, CKSIG_ENOTFILE); blob_zero(&sql); db_begin_transaction(); if( zFrom ){ @@ -819,11 +856,11 @@ }else{ cmp = fossil_strcmp(pFromFile->zName, pToFile->zName); } if( cmp<0 ){ if( file_dir_match(pFileDir, pFromFile->zName) ){ - if( (diffFlags & DIFF_NUMSTAT)==0 ){ + if( (diffFlags & (DIFF_NUMSTAT|DIFF_HTML))==0 ){ fossil_print("DELETED %s\n", pFromFile->zName); } if( asNewFlag ){ diff_manifest_entry(pFromFile, 0, zDiffCmd, zBinGlob, fIncludeBinary, diffFlags); @@ -830,11 +867,11 @@ } } pFromFile = manifest_file_next(pFrom,0); }else if( cmp>0 ){ if( file_dir_match(pFileDir, pToFile->zName) ){ - if( (diffFlags & DIFF_NUMSTAT)==0 ){ + if( (diffFlags & (DIFF_NUMSTAT|DIFF_HTML|DIFF_TCL|DIFF_JSON))==0 ){ fossil_print("ADDED %s\n", pToFile->zName); } if( asNewFlag ){ diff_manifest_entry(0, pToFile, zDiffCmd, zBinGlob, fIncludeBinary, diffFlags); @@ -901,12 +938,13 @@ Blob script; const char *zTempFile = 0; char *zCmd; const char *zTclsh; blob_zero(&script); - blob_appendf(&script, "set fossilcmd {| \"%/\" %s --html -y -i -v", + blob_appendf(&script, "set fossilcmd {| \"%/\" %s -tcl -i -v", g.nameOfExe, zSubCmd); + find_option("tcl",0,0); find_option("html",0,0); find_option("side-by-side","y",0); find_option("internal","i",0); find_option("verbose","v",0); zTclsh = find_option("tclsh",0,1); Index: src/fileedit.c ================================================================== --- src/fileedit.c +++ src/fileedit.c @@ -1999,11 +1999,11 @@ ** fossil.page.fileedit.js. Potential TODO: move this into the ** window.fossil bootstrapping so that we don't have to "fulfill" ** the JS multiple times. */ ajax_emit_js_preview_modes(1); - builtin_request_js("sbsdiff.js"); + builtin_request_js("diff.js"); builtin_request_js("fossil.page.fileedit.js"); builtin_fulfill_js_requests(); { /* Dynamically populate the editor, display any error in the err ** blob, and/or switch to tab #0, where the file selector Index: src/info.c ================================================================== --- src/info.c +++ src/info.c @@ -441,11 +441,11 @@ /* ** Generate javascript to enhance HTML diffs. */ void append_diff_javascript(int sideBySide){ if( !sideBySide ) return; - builtin_request_js("sbsdiff.js"); + builtin_request_js("diff.js"); } /* ** Construct an appropriate diffFlag for text_diff() based on query ** parameters and the to boolean arguments. Index: src/main.mk ================================================================== --- src/main.mk +++ src/main.mk @@ -218,10 +218,11 @@ $(SRCDIR)/alerts/plunk.wav \ $(SRCDIR)/chat.js \ $(SRCDIR)/ci_edit.js \ $(SRCDIR)/copybtn.js \ $(SRCDIR)/default.css \ + $(SRCDIR)/diff.js \ $(SRCDIR)/diff.tcl \ $(SRCDIR)/forum.js \ $(SRCDIR)/fossil.bootstrap.js \ $(SRCDIR)/fossil.confirmer.js \ $(SRCDIR)/fossil.copybutton.js \ @@ -244,11 +245,10 @@ $(SRCDIR)/hbmenu.js \ $(SRCDIR)/href.js \ $(SRCDIR)/login.js \ $(SRCDIR)/markdown.md \ $(SRCDIR)/menu.js \ - $(SRCDIR)/sbsdiff.js \ $(SRCDIR)/scroll.js \ $(SRCDIR)/skin.js \ $(SRCDIR)/sorttable.js \ $(SRCDIR)/sounds/0.wav \ $(SRCDIR)/sounds/1.wav \ DELETED src/sbsdiff.js Index: src/sbsdiff.js ================================================================== --- src/sbsdiff.js +++ /dev/null @@ -1,63 +0,0 @@ -/* The javascript in this file was added by Joel Bruick on 2013-07-06, -** originally as in-line javascript. It keeps the horizontal scrollbars -** in sync on side-by-side diffs. -*/ -(function(){ - var SCROLL_LEN = 25; - function initSbsDiff(diff){ - var txtCols = diff.querySelectorAll('.difftxtcol'); - var txtPres = diff.querySelectorAll('.difftxtcol pre'); - var width = Math.max(txtPres[0].scrollWidth, txtPres[1].scrollWidth); - var i; - for(i=0; i<2; i++){ - txtPres[i].style.width = width + 'px'; - txtCols[i].onscroll = function(e){ - txtCols[0].scrollLeft = txtCols[1].scrollLeft = this.scrollLeft; - }; - } - diff.tabIndex = 0; - diff.onkeydown = function(e){ - e = e || event; - var len = {37: -SCROLL_LEN, 39: SCROLL_LEN}[e.keyCode]; - if( !len ) return; - txtCols[0].scrollLeft += len; - return false; - }; - } - var i, diffs = document.querySelectorAll('.sbsdiffcols') - /* Maintenance reminder: using forEach() here breaks - MSIE<=11, and we need to keep those browsers working on - the /info page. */; - for(i=0; i"/*#wikiedit-tab-save*/); } builtin_fossil_js_bundle_or("fetch", "dom", "tabs", "confirmer", "storage", "popupwidget", "copybutton", "pikchr", NULL); - builtin_request_js("sbsdiff.js"); + builtin_request_js("diff.js"); builtin_request_js("fossil.page.wikiedit.js"); builtin_fulfill_js_requests(); /* Dynamically populate the editor... */ style_script_begin(__FILE__,__LINE__); { Index: test/diff-test-1.wiki ================================================================== --- test/diff-test-1.wiki +++ test/diff-test-1.wiki @@ -20,11 +20,14 @@ The edit of a line with multibyte characters is the first chunk. * Large diff of sqlite3.c. This diff was very slow prior to the performance enhancement change [9e15437e97]. * - A difficult indentation change. + A difficult indentation change. UPDATE: Notice also the improved + multi-segment update marks on lines 122648 and 122763 on the new side. + * Inverse of the previous. * Another tricky indentation. Notice especially lines 59398 and 59407 on the left. * Inverse of the previous. Index: win/Makefile.mingw ================================================================== --- win/Makefile.mingw +++ win/Makefile.mingw @@ -627,10 +627,11 @@ $(SRCDIR)/alerts/plunk.wav \ $(SRCDIR)/chat.js \ $(SRCDIR)/ci_edit.js \ $(SRCDIR)/copybtn.js \ $(SRCDIR)/default.css \ + $(SRCDIR)/diff.js \ $(SRCDIR)/diff.tcl \ $(SRCDIR)/forum.js \ $(SRCDIR)/fossil.bootstrap.js \ $(SRCDIR)/fossil.confirmer.js \ $(SRCDIR)/fossil.copybutton.js \ @@ -653,11 +654,10 @@ $(SRCDIR)/hbmenu.js \ $(SRCDIR)/href.js \ $(SRCDIR)/login.js \ $(SRCDIR)/markdown.md \ $(SRCDIR)/menu.js \ - $(SRCDIR)/sbsdiff.js \ $(SRCDIR)/scroll.js \ $(SRCDIR)/skin.js \ $(SRCDIR)/sorttable.js \ $(SRCDIR)/sounds/0.wav \ $(SRCDIR)/sounds/1.wav \ Index: win/Makefile.msc ================================================================== --- win/Makefile.msc +++ win/Makefile.msc @@ -569,10 +569,11 @@ "$(SRCDIR)\alerts\plunk.wav" \ "$(SRCDIR)\chat.js" \ "$(SRCDIR)\ci_edit.js" \ "$(SRCDIR)\copybtn.js" \ "$(SRCDIR)\default.css" \ + "$(SRCDIR)\diff.js" \ "$(SRCDIR)\diff.tcl" \ "$(SRCDIR)\forum.js" \ "$(SRCDIR)\fossil.bootstrap.js" \ "$(SRCDIR)\fossil.confirmer.js" \ "$(SRCDIR)\fossil.copybutton.js" \ @@ -595,11 +596,10 @@ "$(SRCDIR)\hbmenu.js" \ "$(SRCDIR)\href.js" \ "$(SRCDIR)\login.js" \ "$(SRCDIR)\markdown.md" \ "$(SRCDIR)\menu.js" \ - "$(SRCDIR)\sbsdiff.js" \ "$(SRCDIR)\scroll.js" \ "$(SRCDIR)\skin.js" \ "$(SRCDIR)\sorttable.js" \ "$(SRCDIR)\sounds\0.wav" \ "$(SRCDIR)\sounds\1.wav" \ @@ -1177,10 +1177,11 @@ echo "$(SRCDIR)\alerts/plunk.wav" >> $@ echo "$(SRCDIR)\chat.js" >> $@ echo "$(SRCDIR)\ci_edit.js" >> $@ echo "$(SRCDIR)\copybtn.js" >> $@ echo "$(SRCDIR)\default.css" >> $@ + echo "$(SRCDIR)\diff.js" >> $@ echo "$(SRCDIR)\diff.tcl" >> $@ echo "$(SRCDIR)\forum.js" >> $@ echo "$(SRCDIR)\fossil.bootstrap.js" >> $@ echo "$(SRCDIR)\fossil.confirmer.js" >> $@ echo "$(SRCDIR)\fossil.copybutton.js" >> $@ @@ -1203,11 +1204,10 @@ echo "$(SRCDIR)\hbmenu.js" >> $@ echo "$(SRCDIR)\href.js" >> $@ echo "$(SRCDIR)\login.js" >> $@ echo "$(SRCDIR)\markdown.md" >> $@ echo "$(SRCDIR)\menu.js" >> $@ - echo "$(SRCDIR)\sbsdiff.js" >> $@ echo "$(SRCDIR)\scroll.js" >> $@ echo "$(SRCDIR)\skin.js" >> $@ echo "$(SRCDIR)\sorttable.js" >> $@ echo "$(SRCDIR)\sounds/0.wav" >> $@ echo "$(SRCDIR)\sounds/1.wav" >> $@ ADDED www/css/diff.md Index: www/css/diff.md ================================================================== --- /dev/null +++ www/css/diff.md @@ -0,0 +1,112 @@ +Notes On Diff Formatting +======================== + +There are two main kinds of diff display for the web interface: +unified and side-by-side. Both displays are implemented using +a <table>. The unified diff is a 4-column table, and the +side-by-side diff is a 5-column table. In a page like /info that +might show multiple file diffs, each file diff is in a separate +<table>. For side-by-side diffs, a small amount of Javascript +code is used to resize the text columns so that they fill the screen +width and to keep horizontal scrollbars in sync. + +For the unified diff, the basic structure +is like this: + +> ~~~~ + + + + + + + +
+     Line numbers for the left-hand file
+  
+     Line numbers for the right-hand file
+  
+     Change marks.  "+" or "=" or nothing
+  
+     The text
+  
+~~~~ + +The structure for a side-by-side diff follows the +same basic pattern, though with 5 columns instead of +4, and slightly different class names: + +> ~~~~ + + + + + + + + +
+     Line numbers for the left-hand file
+  
+     The text for the left side
+  
+     Change marks.  "+" or "=" or nothing
+  
+     Line numbers for the right-hand file
+  
+     The text on the right-hand side
+  
+~~~~ + +The outer <table> always has class "diff". The "diffu" class +is added for unified diffs and the "splitdiff" class is added for +side-by-side diffs. + +All line-number columns have the "diffln" class. They also always +have one of "difflnl" or "difflnr" depending on whether they hold +line numbers for the left or right files, respectively. + +Text is always kept in a separate column so that it can be scraped +and copied by the user. All text columns have the "difftxt" class. +One additional class "difftxtu", "difftxtl", or "difftxtr" is added +depending on if the text is for a unified diff, the left column of +a side-by-side diff, or the right column of a side-by-side diff. + +The content of all columns is a single <pre> that contains the +appropriate diff-text for that column. Scrolling is done on the +<pre> element. + +Within text columns, highlighting is done with <del> and +<ins> markup. All text on a line that contains an isert or +delete is surrounded by <ins>...</ins> or +<del>..</del>. Within that line, specific characters +of text that specifically inserted deleted have an additional +layer of <ins> or <del> markup. Thus CSS like the +following is appropriate: + +> ~~~~ +td.difftxt ins { + background-color: #dafbe1; /* Light green for the whole line */ + text-decoration: none; +} +td.difftxt ins > ins { + background-color: #a0e4b2; /* Dark green for specific characters that change */ + text-decoration: none; +} +~~~~ + +In a side-by-side diff, if an interior <ins> or <del> that mark +specific characters that change correspond to a delete/insert on the other +side, they they have the "edit" class tag. (ex: <ins class='edit'> +or <del class='edit'>). Some skins choose to paint these "modified" +regions blue: + +> ~~~~ +td.difftxt ins > ins.edit { + background-color: #c0c0ff; /* Blue for "modified" text region */ + text-decoration: none; +} +~~~~ + +Line number text also has <ins> and <del> tags for lines which +are pure insert or pure delete. But the tags do not nest for line numbers. ADDED www/css/index.md Index: www/css/index.md ================================================================== --- /dev/null +++ www/css/index.md @@ -0,0 +1,12 @@ +Cascading Style Sheet Notes +=========================== + +This is a collection of technical notes that document the design of +the Document Object Model (DOM) and corresponding Cascading Style Sheet (CSS) +attributes used for customing the look-and-feel of Fossil. These notes +are of interest to people who want to customize the Fossil skin or +enhance the internal display logic. + +This is a collection of documents that we hope will grow over time. + + * [Diff styling](./diff.md)