Index: src/diff.c ================================================================== --- src/diff.c +++ src/diff.c @@ -355,19 +355,21 @@ typedef struct SbsLine SbsLine; struct SbsLine { char *zLine; /* The output line under construction */ int n; /* Index of next unused slot in the zLine[] */ int width; /* Maximum width of a column in the output */ - unsigned char escHtml; /* True to escape html characters */ + unsigned char escHtml; /* True to escape html characters */ + int iStart; /* Write zStart prior to character iStart */ + const char *zStart; /* A <span> tag */ + int iEnd; /* Write </span> prior to character iEnd */ }; /* ** Flags for sbsWriteText() */ -#define SBS_NEWLINE 0x0001 /* End with \n\000 */ -#define SBS_PAD 0x0002 /* Pad output to width spaces */ -#define SBS_ENDSPAN 0x0004 /* Write a </span> after text */ +#define SBS_NEWLINE 0x0001 /* End with \n\000 */ +#define SBS_PAD 0x0002 /* Pad output to width spaces */ /* ** Write up to width characters of pLine into z[]. Translate tabs into ** spaces. Add a newline if SBS_NEWLINE is set. Translate HTML characters ** if SBS_HTML is set. Pad the rendering out width bytes if SBS_PAD is set. @@ -380,10 +382,20 @@ const char *zIn = pLine->z; char *z = &p->zLine[p->n]; int w = p->width; for(i=j=k=0; k<w && i<n; i++, k++){ char c = zIn[i]; + if( p->escHtml ){ + if( i==p->iStart ){ + int x = strlen(p->zStart); + memcpy(z+j, p->zStart, x); + j += x; + }else if( i==p->iEnd ){ + memcpy(z+j, "</span>", 7); + j += 7; + } + } if( c=='\t' ){ z[j++] = ' '; while( (k&7)!=7 && k<w ){ z[j++] = ' '; k++; } }else if( c=='\r' || c=='\f' ){ z[j++] = ' '; @@ -398,11 +410,11 @@ j += 4; }else{ z[j++] = c; } } - if( (flags & SBS_ENDSPAN) && p->escHtml ){ + if( p->escHtml && i<=p->iEnd ){ memcpy(&z[j], "</span>", 7); j += 7; } if( (flags & SBS_PAD)!=0 ){ while( k<w ){ k++; z[j++] = ' '; } @@ -444,10 +456,226 @@ p->n += 6; sbsWriteHtml(p, "</span>"); p->zLine[p->n++] = ' '; } +/* +** 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 nLeft; /* Length of left line in bytes */ + int nRight; /* Length of right line in bytes */ + int nPrefix; /* Length of common prefix */ + int nSuffix; /* Length of common suffix */ + int width; /* Total column width */ + const char *zLeft; /* Text of the left line */ + const char *zRight; /* Text of the right line */ + + nLeft = pLeft->h & LENGTH_MASK; + zLeft = pLeft->z; + nRight = pRight->h & LENGTH_MASK; + zRight = pRight->z; + + nPrefix = 0; + while( nPrefix<nLeft && nPrefix<nRight && zLeft[nPrefix]==zRight[nPrefix] ){ + nPrefix++; + } + nSuffix = 0; + if( nPrefix<nLeft && nPrefix<nRight ){ + while( nSuffix<nLeft && nSuffix<nRight + && zLeft[nLeft-nSuffix-1]==zRight[nRight-nSuffix-1] ){ + nSuffix++; + } + if( nSuffix==nLeft || nSuffix==nRight ) nPrefix = 0; + } + if( nPrefix+nSuffix > nLeft ) nSuffix = nLeft - nPrefix; + if( nPrefix+nSuffix > nRight ) nSuffix = nRight - nPrefix; + if( nPrefix+nSuffix==nLeft ){ + /* Text inserted on the right */ + sbsWriteLineno(p, lnLeft); + p->iStart = p->iEnd = -1; + sbsWriteText(p, pLeft, SBS_PAD); + sbsWrite(p, " | ", 3); + sbsWriteLineno(p, lnRight); + p->iStart = nPrefix; + p->iEnd = nRight - nSuffix; + p->zStart = "<span class=\"diffadd\">"; + sbsWriteText(p, pRight, SBS_NEWLINE); + }else if( nPrefix+nSuffix==nRight ){ + /* Text deleted from the left */ + sbsWriteLineno(p, lnLeft); + p->iStart = nPrefix; + p->iEnd = nLeft - nSuffix; + p->zStart = "<span class=\"diffrm\">"; + sbsWriteText(p, pLeft, SBS_PAD); + sbsWrite(p, " | ", 3); + sbsWriteLineno(p, lnRight); + p->iStart = p->iEnd = -1; + sbsWriteText(p, pRight, SBS_NEWLINE); + }else{ + /* Text modified between left and right */ + sbsWriteLineno(p, lnLeft); + p->iStart = nPrefix; + p->iEnd = nLeft - nSuffix; + p->zStart = "<span class=\"diffchng\">"; + sbsWriteText(p, pLeft, SBS_PAD); + sbsWrite(p, " | ", 3); + sbsWriteLineno(p, lnRight); + p->iEnd = nRight - nSuffix; + sbsWriteText(p, pRight, SBS_NEWLINE); + } +} + +/* +** 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. +*/ +static int match_dline(DLine *pA, DLine *pB){ + const char *zA; + const char *zB; + int nA; + int nB; + int minDist; + int i; + int nMatch; + int score; + + zA = pA->z; + zB = pB->z; + nA = pA->h & LENGTH_MASK; + nB = pB->h & LENGTH_MASK; + while( nA>0 && fossil_isspace(zA[0]) ){ nA--; zA++; } + while( nA>0 && fossil_isspace(zA[nA-1]) ){ nA--; } + while( nB>0 && fossil_isspace(zB[0]) ){ nB--; zB++; } + while( nB>0 && fossil_isspace(zB[nB-1]) ){ nB--; } + minDist = nA; + if( minDist>nB ) minDist = nB; + if( minDist==0 ){ + score = 100; + }else{ + for(i=0; i<nA && i<nB && zA[i]==zB[i]; i++){} + nMatch = i; + for(i=1; i<=nA && i<=nB && zA[nA-i]==zB[nB-i]; i++){} + nMatch += i-1; + score = (nMatch >= minDist) ? 0 : ((minDist - nMatch)*100)/minDist; + } + return score; +} + +/* +** 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. +** +** The return value is a buffer of unsigned characters, obtained from +** fossil_malloc(). (The caller needs to free the return value using +** fossil_free().) Entries in the returned array have values as follows: +** +** 1. Delete the next line of pLeft. +** 2. The next line of pLeft changes into the next line of pRight. +** 3. Insert the next line of pRight. +** +** The length of the returned array will be just large enough to cause +** all elements of pLeft and pRight to be consumed. +** +** 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 */ +){ + 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 aBuf[100]; /* Stack space for a[] if nRight not to big */ + + aM = fossil_malloc( (nLeft+1)*(nRight+1) ); + if( nLeft==0 ){ + memset(aM, 3, nRight); + return aM; + } + if( nRight==0 ){ + memset(aM, 1, nLeft); + return aM; + } + if( nRight < (sizeof(aBuf)/sizeof(aBuf[0]))-1 ){ + pToFree = 0; + a = aBuf; + }else{ + a = pToFree = fossil_malloc( sizeof(a[0])*(nRight+1) ); + } + + /* Compute the best alignment */ + for(i=0; i<=nRight; i++){ + aM[i] = 3; + a[i] = i*50; + } + aM[0] = 0; + for(j=1; j<=nLeft; j++){ + int p = a[0]; + a[0] = p+50; + aM[j*(nRight+1)] = 1; + for(i=1; i<=nRight; i++){ + int m = a[i-1]+50; + int d = 3; + if( m>a[i]+50 ){ + m = a[i]+50; + d = 1; + } + if( m>p ){ + int score = match_dline(&aLeft[j-1], &aRight[i-1]); + if( (score<66 || (i<j+1 && i>j-1)) && m>p+score ){ + m = p+score; + d = 2; + } + } + p = a[i]; + a[i] = m; + aM[j*(nRight+1)+i] = d; + } + } + + /* Compute the lowest-cost path back through the matrix */ + i = nRight; + j = nLeft; + k = (nRight+1)*(nLeft+1)-1; + while( i+j>0 ){ + unsigned char c = aM[k--]; + if( c==2 ){ + assert( i>0 && j>0 ); + i--; + j--; + }else if( c==3 ){ + assert( i>0 ); + i--; + }else{ + assert( j>0 ); + j--; + } + aM[k] = aM[j*(nRight+1)+i]; + } + k++; + i = (nRight+1)*(nLeft+1) - k; + memmove(aM, &aM[k], i); + + /* Return the result */ + fossil_free(pToFree); + return aM; +} /* ** Given a diff context in which the aEdit[] array has been filled ** in, compute a side-by-side diff into pOut. */ @@ -474,10 +702,12 @@ s.zLine = fossil_malloc( 10*width + 100 ); if( s.zLine==0 ) return; s.width = width; s.escHtml = escHtml; + s.iStart = -1; + s.iEnd = -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; } @@ -527,10 +757,11 @@ b += skip; m = R[r] - skip; for(j=0; j<m; j++){ s.n = 0; sbsWriteLineno(&s, a+j); + s.iStart = s.iEnd = -1; sbsWriteText(&s, &A[a+j], SBS_PAD); sbsWrite(&s, " ", 3); sbsWriteLineno(&s, b+j); sbsWriteText(&s, &B[b+j], SBS_NEWLINE); blob_append(pOut, s.zLine, s.n); @@ -538,52 +769,58 @@ a += m; b += m; /* Show the differences */ for(i=0; i<nr; i++){ - ma = R[r+i*3+1]; - mb = R[r+i*3+2]; - m = ma<mb ? ma : mb; - for(j=0; j<m; j++){ - s.n = 0; - sbsWriteLineno(&s, a+j); - sbsWriteHtml(&s, "<span class=\"diffchng\">"); - sbsWriteText(&s, &A[a+j], SBS_PAD | SBS_ENDSPAN); - sbsWrite(&s, " | ", 3); - sbsWriteLineno(&s, b+j); - sbsWriteHtml(&s, "<span class=\"diffchng\">"); - sbsWriteText(&s, &B[b+j], SBS_NEWLINE | SBS_ENDSPAN); - blob_append(pOut, s.zLine, s.n); - } - a += m; - b += m; - ma -= m; - mb -= m; - for(j=0; j<ma; j++){ - s.n = 0; - sbsWriteLineno(&s, a+j); - sbsWriteHtml(&s, "<span class=\"diffrm\">"); - sbsWriteText(&s, &A[a+j], SBS_PAD | SBS_ENDSPAN); - sbsWrite(&s, " <\n", 3); - blob_append(pOut, s.zLine, s.n); + unsigned char *alignment; + ma = R[r+i*3+1]; /* Lines on left but not on right */ + mb = R[r+i*3+2]; /* Lines on right but not on left */ + alignment = sbsAlignment(&A[a], ma, &B[b], mb); + for(j=0; ma+mb>0; j++){ + if( alignment[j]==1 ){ + s.n = 0; + sbsWriteLineno(&s, a); + s.iStart = 0; + s.zStart = "<span class=\"diffrm\">"; + s.iEnd = s.width; + sbsWriteText(&s, &A[a], SBS_PAD); + sbsWrite(&s, " <\n", 3); + blob_append(pOut, s.zLine, s.n); + assert( ma>0 ); + ma--; + a++; + }else if( alignment[j]==2 ){ + s.n = 0; + sbsWriteLineChange(&s, &A[a], a, &B[b], b); + blob_append(pOut, s.zLine, s.n); + assert( ma>0 && mb>0 ); + ma--; + mb--; + a++; + b++; + }else{ + s.n = 0; + sbsWriteSpace(&s, width + 7); + sbsWrite(&s, " > ", 3); + sbsWriteLineno(&s, b); + s.iStart = 0; + s.zStart = "<span class=\"diffadd\">"; + s.iEnd = s.width; + sbsWriteText(&s, &B[b], SBS_NEWLINE); + blob_append(pOut, s.zLine, s.n); + assert( mb>0 ); + mb--; + b++; + } } - a += ma; - for(j=0; j<mb; j++){ - s.n = 0; - sbsWriteSpace(&s, width + 7); - sbsWrite(&s, " > ", 3); - sbsWriteLineno(&s, b+j); - sbsWriteHtml(&s, "<span class=\"diffadd\">"); - sbsWriteText(&s, &B[b+j], SBS_NEWLINE | SBS_ENDSPAN); - blob_append(pOut, s.zLine, s.n); - } - b += mb; + fossil_free(alignment); if( i<nr-1 ){ m = R[r+i*3+3]; for(j=0; j<m; j++){ s.n = 0; sbsWriteLineno(&s, a+j); + s.iStart = s.iEnd = -1; sbsWriteText(&s, &A[a+j], SBS_PAD); sbsWrite(&s, " ", 3); sbsWriteLineno(&s, b+j); sbsWriteText(&s, &B[b+j], SBS_NEWLINE); blob_append(pOut, s.zLine, s.n); @@ -598,10 +835,11 @@ m = R[r+nr*3]; if( m>nContext ) m = nContext; for(j=0; j<m; j++){ s.n = 0; sbsWriteLineno(&s, a+j); + s.iStart = s.iEnd = -1; sbsWriteText(&s, &A[a+j], SBS_PAD); sbsWrite(&s, " ", 3); sbsWriteLineno(&s, b+j); sbsWriteText(&s, &B[b+j], SBS_NEWLINE); blob_append(pOut, s.zLine, s.n); @@ -943,11 +1181,11 @@ p->aEdit[r+3]++; cpy--; } /* Shift insertions toward the end of the file */ - while( p->aEdit[r+3]>0 && del==0 && ins>0 ){ + while( r+3<p->nEdit && p->aEdit[r+3]>0 && del==0 && ins>0 ){ DLine *pTop = &p->aTo[lnTo]; /* First line inserted */ DLine *pBtm = &p->aTo[lnTo+ins]; /* First line past end of insert */ if( same_dline(pTop, pBtm)==0 ) break; if( LENGTH(pTop)+LENGTH(pBtm-1)<=LENGTH(pTop+1)+LENGTH(pBtm) ) break; lnFrom++; @@ -969,11 +1207,11 @@ p->aEdit[r+3]++; cpy--; } /* Shift deletions toward the end of the file */ - while( p->aEdit[r+3]>0 && del>0 && ins==0 ){ + while( r+3<p->nEdit && p->aEdit[r+3]>0 && del>0 && ins==0 ){ DLine *pTop = &p->aFrom[lnFrom]; /* First line deleted */ DLine *pBtm = &p->aFrom[lnFrom+del]; /* First line past end of delete */ if( same_dline(pTop, pBtm)==0 ) break; if( LENGTH(pTop)+LENGTH(pBtm-1)<=LENGTH(pTop)+LENGTH(pBtm) ) break; lnFrom++; Index: src/style.c ================================================================== --- src/style.c +++ src/style.c @@ -764,11 +764,11 @@ @ font-family: monospace; @ white-space: pre; }, { "span.diffchng", "changes in a diff", - @ background-color: #ffffc8; + @ background-color: #e0e0ff; }, { "span.diffadd", "added code in a diff", @ background-color: #e0ffe0; },