mirror of
https://gitlab.com/freepascal.org/lazarus/lazarus.git
synced 2025-04-07 19:58:15 +02:00
909 lines
28 KiB
ObjectPascal
909 lines
28 KiB
ObjectPascal
{
|
|
/***************************************************************************
|
|
diffpatch.pas - functions to extract differences between texts
|
|
(diffs, patches) and apply them (patching).
|
|
|
|
***************************************************************************/
|
|
|
|
***************************************************************************
|
|
* *
|
|
* This source is free software; you can redistribute it and/or modify *
|
|
* it under the terms of the GNU General Public License as published by *
|
|
* the Free Software Foundation; either version 2 of the License, or *
|
|
* (at your option) any later version. *
|
|
* *
|
|
* This code is distributed in the hope that it will be useful, but *
|
|
* WITHOUT ANY WARRANTY; without even the implied warranty of *
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
|
|
* General Public License for more details. *
|
|
* *
|
|
* A copy of the GNU General Public License is available on the World *
|
|
* Wide Web at <http://www.gnu.org/copyleft/gpl.html>. You can also *
|
|
* obtain it by writing to the Free Software Foundation, *
|
|
* Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1335, USA. *
|
|
* *
|
|
***************************************************************************
|
|
|
|
Author: Mattias Gaertner
|
|
|
|
Abstract:
|
|
Methods for creating diffs and ToDo: applying them (patching).
|
|
|
|
}
|
|
unit DiffPatch;
|
|
|
|
{$mode objfpc}{$H+}
|
|
|
|
interface
|
|
|
|
uses
|
|
Classes, SysUtils, LazLoggerBase;
|
|
|
|
type
|
|
TTextDiffFlag = (
|
|
tdfIgnoreCase, // ignore case of letters
|
|
tdfIgnoreEmptyLineChanges,// ignore if empty lines were added or removed
|
|
tdfIgnoreHeadingSpaces, // ignore spaces at start of line
|
|
tdfIgnoreLineEnds, // ignore if line end chars differ (e.g. #10 = #13#10)
|
|
tdfIgnoreSpaceCharAmount, // ignore if space chars were added or removed
|
|
// except if all spaces were removed
|
|
tdfIgnoreSpaceChars, // ignore spaces (newline chars not included)
|
|
tdfIgnoreTrailingSpaces // ignore spaces at end of line
|
|
);
|
|
TTextDiffFlags = set of TTextDiffFlag;
|
|
|
|
TTextDiffOutputType = (
|
|
tdoContext,
|
|
tdoRCS
|
|
);
|
|
|
|
TLineExtends = record
|
|
LineStart: integer;
|
|
LineEnd: integer;
|
|
LineNumber: integer; // starting at 1
|
|
NextLineStart: integer;
|
|
end;
|
|
|
|
TDiffOutput = class;
|
|
|
|
{ TDiffPart }
|
|
|
|
TDiffPart = class
|
|
private
|
|
fOwner: TDiffOutput;
|
|
fSource: string;
|
|
fStartLine: integer; // starting at 1
|
|
fEndLine: integer; // starting at 1
|
|
fPosition: TLineExtends;
|
|
fStream: TStream;
|
|
public
|
|
constructor Create(aOwner: TDiffOutput; const aSource: string);
|
|
destructor Destroy; override;
|
|
procedure Init(StartExt: TLineExtends);
|
|
procedure Write(const HeaderPrefix, HeaderSuffix: string);
|
|
procedure Write2(const StartExt, EndExt: TLineExtends;
|
|
OtherPartHasChangedLines: boolean; CharForInsertDeletion: char);
|
|
procedure WriteLinesOfText(aStream: TStream; const aPrefix: string;
|
|
const aStartLine: TLineExtends; aEndPos: integer);
|
|
function LineExtendsToStr(const LineExtends: TLineExtends): string;
|
|
procedure GetLineExtends(LineStart: integer; var LineEnd, NextLineStart: integer);
|
|
procedure GetLineExtends(var LineExtends: TLineExtends);
|
|
procedure GetPrevLineExtends(LineStart: integer; var PrevLineStart, PrevLineEnd: integer);
|
|
procedure GetPrevLineExtends(var LineExtends: TLineExtends);
|
|
function CountLineEnds(StartPos, EndPos: integer): integer;
|
|
function CountLinesTillEnd(StartPos: integer): integer;
|
|
function IsEmptyLine(LineStart, LineEnd: integer): boolean;
|
|
procedure GetNextLineExtends(var LineStart, LineEnd, NextLineStart, LineNumber: integer);
|
|
procedure GetNextLineExtends(var LineExtends: TLineExtends);
|
|
end;
|
|
|
|
TProgressEvent = procedure(aPosition: Integer) of object;
|
|
|
|
{ TDiffOutput }
|
|
|
|
TDiffOutput = class
|
|
private
|
|
fText1, fText2: string;
|
|
fOutputType: TTextDiffOutputType;
|
|
fOnProgressPos: TProgressEvent;
|
|
fFlags: TTextDiffFlags;
|
|
fDiffStream: TStream;
|
|
fPart1, fPart2: TDiffPart;
|
|
procedure FindNextEqualLine(const Start1, Start2: TLineExtends;
|
|
out EqualLine1, EqualLine2: TLineExtends);
|
|
function LinesAreEqual(Line1Start, Line1End, NextLine1Start: integer;
|
|
Line2Start, Line2End, NextLine2Start: integer): boolean;
|
|
function LinesAreEqual(const Line1Extends, Line2Extends: TLineExtends): boolean;
|
|
procedure AddDefaultDiff(const Start1, End1, Start2, End2: TLineExtends);
|
|
procedure AddContextDiff(const Start1, End1, Start2, End2: TLineExtends);
|
|
procedure FinishOldContextBlock;
|
|
procedure FinishDiff;
|
|
procedure AddRestDiff(const Start1, Start2: TLineExtends);
|
|
procedure AddDiff(const Start1, End1, Start2, End2: TLineExtends);
|
|
procedure UpdateProgressBar(const Line: TLineExtends);
|
|
public
|
|
constructor Create(const aText1, aText2: string; aFlags: TTextDiffFlags);
|
|
destructor Destroy; override;
|
|
function GetProgressMax: Integer;
|
|
function CreateTextDiff: string;
|
|
public
|
|
property OutputType: TTextDiffOutputType read fOutputType write fOutputType;
|
|
property OnProgressPos: TProgressEvent read fOnProgressPos write fOnProgressPos;
|
|
end;
|
|
|
|
|
|
const
|
|
TextDiffFlagNames: array[TTextDiffFlag] of string = (
|
|
'IgnoreCase',
|
|
'IgnoreEmptyLineChanges',
|
|
'IgnoreHeadingSpaces',
|
|
'IgnoreLineEnds',
|
|
'IgnoreSpaceCharAmount',
|
|
'IgnoreSpaceChars',
|
|
'IgnoreTrailingSpaces'
|
|
);
|
|
|
|
|
|
implementation
|
|
|
|
const
|
|
LineBreak = #10;
|
|
ContextLineCount = 3;
|
|
|
|
var
|
|
IsSpaceChars: array[char] of boolean;
|
|
UpperCaseChars: array[char] of char;
|
|
|
|
function TextToLine(const s: string): string;
|
|
var
|
|
i: integer;
|
|
OrdStr: string;
|
|
begin
|
|
Result:=s;
|
|
i:=1;
|
|
while i<=length(Result) do begin
|
|
if ord(Result[i])>=ord(' ') then begin
|
|
inc(i);
|
|
end else begin
|
|
OrdStr:='#'+IntToStr(ord(Result[i]));
|
|
Result:=LeftStr(Result,i-1)+OrdStr+RightStr(Result,length(Result)-i);
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
function GotoNextLine(var LineExtends: TLineExtends): boolean;
|
|
begin
|
|
with LineExtends do begin
|
|
Result:=LineStart<NextLineStart;
|
|
if Result then begin
|
|
inc(LineNumber);
|
|
LineStart:=NextLineStart;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
procedure WriteStrToStream(Stream: TStream; const s: string);
|
|
begin
|
|
if s='' then exit;
|
|
Stream.Write(s[1],length(s));
|
|
end;
|
|
|
|
{ TDiffPart }
|
|
|
|
constructor TDiffPart.Create(aOwner: TDiffOutput; const aSource: string);
|
|
begin
|
|
fOwner:=aOwner;
|
|
fSource:=aSource;
|
|
end;
|
|
|
|
destructor TDiffPart.Destroy;
|
|
begin
|
|
fStream.Free;
|
|
inherited Destroy;
|
|
end;
|
|
|
|
procedure TDiffPart.Init(StartExt: TLineExtends);
|
|
begin
|
|
if fStream=nil then
|
|
fStream:=TMemoryStream.Create
|
|
else
|
|
fStream.Size:=0;
|
|
fStartLine:=StartExt.LineNumber-ContextLineCount;
|
|
if fStartLine<1 then fStartLine:=1;
|
|
fPosition:=StartExt;
|
|
while fPosition.LineNumber>fStartLine do
|
|
GetPrevLineExtends(fPosition);
|
|
end;
|
|
|
|
procedure TDiffPart.Write(const HeaderPrefix, HeaderSuffix: string);
|
|
begin
|
|
// check if part contains any changed lines
|
|
if fPosition.LineNumber > fStartLine then begin
|
|
// part contains changed lines -> append end context
|
|
while (fPosition.LineNumber <= fEndLine)
|
|
and (fPosition.LineStart < length(fSource)) do begin
|
|
WriteLinesOfText(fStream, ' ', fPosition, fPosition.NextLineStart);
|
|
if not GotoNextLine(fPosition) then
|
|
break;
|
|
GetLineExtends(fPosition);
|
|
end;
|
|
end else begin
|
|
// part does not contain changed lines -> skip
|
|
end;
|
|
fStream.Position:=0;
|
|
// write part
|
|
WriteStrToStream(fOwner.fDiffStream, HeaderPrefix +
|
|
IntToStr(fStartLine) + ',' + IntToStr(fEndLine) + HeaderSuffix + LineBreak);
|
|
if fStream.Size<>0 then
|
|
fOwner.fDiffStream.CopyFrom(fStream, fStream.Size);
|
|
end;
|
|
|
|
procedure TDiffPart.Write2(const StartExt, EndExt: TLineExtends;
|
|
OtherPartHasChangedLines: boolean; CharForInsertDeletion: char);
|
|
begin
|
|
// check if there are changed lines
|
|
if StartExt.LineStart < EndExt.LineStart then begin
|
|
// write lines
|
|
while fPosition.LineStart < EndExt.LineStart do begin
|
|
if fPosition.LineStart < StartExt.LineStart then
|
|
// this is an unchanged line in front of the changed lines
|
|
WriteStrToStream(fStream, ' ')
|
|
else begin
|
|
// this is a changed line
|
|
if OtherPartHasChangedLines then
|
|
WriteStrToStream(fStream, '! ')
|
|
else
|
|
WriteStrToStream(fStream, CharForInsertDeletion+' ');
|
|
end;
|
|
if fPosition.LineStart <> fPosition.NextLineStart then
|
|
fStream.Write(fSource[fPosition.LineStart], fPosition.NextLineStart-fPosition.LineStart);
|
|
if not GotoNextLine(fPosition) then
|
|
break;
|
|
GetLineExtends(fPosition);
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
procedure TDiffPart.WriteLinesOfText(aStream: TStream;
|
|
const aPrefix: string; const aStartLine: TLineExtends; aEndPos: integer);
|
|
{ Write all lines in front of EndLine, starting with StartLine }
|
|
var
|
|
Line: TLineExtends;
|
|
begin
|
|
Line:=aStartLine;
|
|
while (Line.LineStart<aEndPos) do begin
|
|
WriteStrToStream(aStream,aPrefix);
|
|
if (Line.LineEnd>Line.LineStart) then
|
|
aStream.Write(fSource[Line.LineStart],Line.LineEnd-Line.LineStart);
|
|
if (Line.NextLineStart>Line.LineEnd) then
|
|
aStream.Write(fSource[Line.LineEnd],Line.NextLineStart-Line.LineEnd)
|
|
else begin
|
|
WriteStrToStream(aStream,LineBreak);
|
|
WriteStrToStream(aStream,'\ No newline at end');
|
|
WriteStrToStream(aStream,LineBreak);
|
|
end;
|
|
if not GotoNextLine(Line) then break;
|
|
GetLineExtends(Line);
|
|
end;
|
|
end;
|
|
|
|
function TDiffPart.LineExtendsToStr(const LineExtends: TLineExtends): string;
|
|
begin
|
|
with LineExtends do
|
|
Result:='(Start='+IntToStr(LineStart)+' End='+IntToStr(LineEnd)
|
|
+' Next='+IntToStr(NextLineStart)+' Number='+IntToStr(LineNumber)
|
|
+' Text="'+TextToLine(copy(fSource,LineStart,NextLineStart-LineStart))+'")';
|
|
end;
|
|
|
|
procedure TDiffPart.GetLineExtends(LineStart: integer; var LineEnd, NextLineStart: integer);
|
|
var
|
|
Len: integer;
|
|
begin
|
|
Len:=length(fSource);
|
|
LineEnd:=LineStart;
|
|
while LineEnd<=Len do begin
|
|
if (not (fSource[LineEnd] in [#10,#13])) then begin
|
|
inc(LineEnd);
|
|
end else begin
|
|
NextLineStart:=LineEnd+1;
|
|
if (NextLineStart<=Len) and (fSource[NextLineStart] in [#10,#13])
|
|
and (fSource[LineEnd]<>fSource[NextLineStart]) then
|
|
inc(NextLineStart);
|
|
exit;
|
|
end;
|
|
end;
|
|
// this was the last line and it has no line end
|
|
NextLineStart:=LineEnd;
|
|
end;
|
|
|
|
procedure TDiffPart.GetLineExtends(var LineExtends: TLineExtends);
|
|
begin
|
|
GetLineExtends(LineExtends.LineStart, LineExtends.LineEnd, LineExtends.NextLineStart);
|
|
end;
|
|
|
|
procedure TDiffPart.GetPrevLineExtends(LineStart: integer;
|
|
var PrevLineStart, PrevLineEnd: integer);
|
|
begin
|
|
// read line end
|
|
PrevLineEnd:=LineStart;
|
|
if (PrevLineEnd>1) and (fSource[PrevLineEnd-1] in [#10,#13]) then
|
|
dec(PrevLineEnd);
|
|
if (PrevLineEnd>1) and (fSource[PrevLineEnd-1] in [#10,#13])
|
|
and (fSource[PrevLineEnd]<>fSource[PrevLineEnd-1]) then
|
|
dec(PrevLineEnd);
|
|
// read line content
|
|
PrevLineStart:=PrevLineEnd;
|
|
while (PrevLineStart>1) and (not (fSource[PrevLineStart-1] in [#10,#13])) do
|
|
dec(PrevLineStart);
|
|
end;
|
|
|
|
procedure TDiffPart.GetPrevLineExtends(var LineExtends: TLineExtends);
|
|
begin
|
|
if LineExtends.LineStart<1 then exit;
|
|
LineExtends.NextLineStart:=LineExtends.LineStart;
|
|
GetPrevLineExtends(LineExtends.LineStart,LineExtends.LineStart,LineExtends.LineEnd);
|
|
dec(LineExtends.LineNumber);
|
|
end;
|
|
|
|
function TDiffPart.CountLineEnds(StartPos, EndPos: integer): integer;
|
|
begin
|
|
Result:=0;
|
|
while (StartPos<EndPos) do begin
|
|
if not (fSource[StartPos] in [#10,#13]) then begin
|
|
inc(StartPos);
|
|
end else begin
|
|
inc(Result);
|
|
inc(StartPos);
|
|
if (StartPos<EndPos) and (fSource[StartPos] in [#10,#13])
|
|
and (fSource[StartPos]<>fSource[StartPos-1]) then
|
|
inc(StartPos);
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
function TDiffPart.CountLinesTillEnd(StartPos: integer): integer;
|
|
var
|
|
Len: integer;
|
|
begin
|
|
Len:=length(fSource);
|
|
Result:=CountLineEnds(StartPos,Len+1);
|
|
if (StartPos<=Len) and (not (fSource[Len] in [#10,#13])) then
|
|
inc(Result);
|
|
end;
|
|
|
|
function TDiffPart.IsEmptyLine(LineStart, LineEnd: integer): boolean;
|
|
var
|
|
i: integer;
|
|
begin
|
|
if LineStart<=length(fSource) then begin
|
|
if ([tdfIgnoreSpaceCharAmount,tdfIgnoreSpaceChars,tdfIgnoreHeadingSpaces,
|
|
tdfIgnoreTrailingSpaces]*fOwner.fFlags)<>[] then
|
|
begin
|
|
Result:=true;
|
|
for i:=LineStart to LineEnd-1 do begin
|
|
if not IsSpaceChars[fSource[i]] then begin
|
|
Result:=false;
|
|
exit;
|
|
end;
|
|
end;
|
|
end else begin
|
|
Result:=(LineEnd=LineStart);
|
|
end;
|
|
end else begin
|
|
Result:=true;
|
|
exit;
|
|
end;
|
|
end;
|
|
|
|
procedure TDiffPart.GetNextLineExtends(var LineStart, LineEnd, NextLineStart, LineNumber: integer);
|
|
var
|
|
Len: integer;
|
|
begin
|
|
Len:=length(fSource);
|
|
repeat
|
|
GetLineExtends(LineStart,LineEnd,NextLineStart);
|
|
if (LineStart>Len)
|
|
or (not (tdfIgnoreEmptyLineChanges in fOwner.fFlags))
|
|
or (not IsEmptyLine(LineStart,LineEnd)) then
|
|
break;
|
|
LineStart:=NextLineStart;
|
|
inc(LineNumber);
|
|
until false;
|
|
end;
|
|
|
|
procedure TDiffPart.GetNextLineExtends(var LineExtends: TLineExtends);
|
|
begin
|
|
GetNextLineExtends(LineExtends.LineStart, LineExtends.LineEnd,
|
|
LineExtends.NextLineStart, LineExtends.LineNumber);
|
|
end;
|
|
|
|
|
|
{ TDiffOutput }
|
|
|
|
function TDiffOutput.LinesAreEqual(
|
|
Line1Start, Line1End, NextLine1Start: integer;
|
|
Line2Start, Line2End, NextLine2Start: integer): boolean;
|
|
var
|
|
Start1, End1, Pos1,
|
|
Start2, End2, Pos2: integer;
|
|
begin
|
|
Start1:=Line1Start;
|
|
End1:=Line1End;
|
|
Start2:=Line2Start;
|
|
End2:=Line2End;
|
|
if [tdfIgnoreHeadingSpaces,tdfIgnoreSpaceChars]*fFlags<>[] then begin
|
|
// ignore spaces at start of line
|
|
while (Start1<End1) and IsSpaceChars[fText1[Start1]] do inc(Start1);
|
|
while (Start2<End2) and IsSpaceChars[fText2[Start2]] do inc(Start2);
|
|
end;
|
|
if [tdfIgnoreTrailingSpaces,tdfIgnoreSpaceChars]*fFlags<>[] then begin
|
|
// ignore spaces at end of line
|
|
while (Start1<End1) and IsSpaceChars[fText1[End1-1]] do dec(End1);
|
|
while (Start2<End2) and IsSpaceChars[fText2[End2-1]] do dec(End2);
|
|
end;
|
|
|
|
// compare line content (i.e. the chars without the line end)
|
|
Pos1:=Start1;
|
|
Pos2:=Start2;
|
|
while (Pos1<End1) and (Pos2<End2) do begin
|
|
if not IsSpaceChars[fText1[Pos1]] then begin
|
|
// fText1 contains a normal char
|
|
if not IsSpaceChars[fText2[Pos2]] then begin
|
|
// fText2 contains a normal char
|
|
if tdfIgnoreCase in fFlags then begin
|
|
// compare case insensitive
|
|
if UpperCaseChars[fText1[Pos1]]=UpperCaseChars[fText2[Pos2]] then begin
|
|
// no diff -> proceed with next chars
|
|
inc(Pos1);
|
|
inc(Pos2);
|
|
end else begin
|
|
// diff found -> lines differ
|
|
Result:=false;
|
|
exit;
|
|
end;
|
|
end else begin
|
|
// compare case sensitive
|
|
if fText1[Pos1]=fText2[Pos2] then begin
|
|
// no diff -> proceed with next chars
|
|
inc(Pos1);
|
|
inc(Pos2);
|
|
end else begin
|
|
// diff found -> lines differ
|
|
Result:=false;
|
|
exit;
|
|
end;
|
|
end;
|
|
end else begin
|
|
// fText2 contains a space
|
|
if not (tdfIgnoreSpaceChars in fFlags) then begin
|
|
// diff found -> lines differ
|
|
Result:=false;
|
|
exit;
|
|
end else begin
|
|
// skip all spaces in fText2 and proceed the search
|
|
repeat
|
|
inc(Pos2);
|
|
until (Pos2>=End2) or (not IsSpaceChars[fText2[Pos2]]);
|
|
end;
|
|
end;
|
|
end else begin
|
|
// fText1 contains a space
|
|
if not IsSpaceChars[fText2[Pos2]] then begin
|
|
// fText2 contains a normal char
|
|
if not (tdfIgnoreSpaceChars in fFlags) then begin
|
|
// diff found -> lines differ
|
|
Result:=false;
|
|
exit;
|
|
end else begin
|
|
// skip all spaces in fText1 and proceed the search
|
|
repeat
|
|
inc(Pos1);
|
|
until (Pos1>=End1) or (not IsSpaceChars[fText1[Pos1]]);
|
|
end;
|
|
end else begin
|
|
// fText2 contains a space
|
|
if [tdfIgnoreSpaceChars,tdfIgnoreSpaceCharAmount]*fFlags<>[] then begin
|
|
// skip all spaces in fText1 and fText2 and proceed the search
|
|
repeat
|
|
inc(Pos1);
|
|
until (Pos1>=End1) or (not IsSpaceChars[fText1[Pos1]]);
|
|
repeat
|
|
inc(Pos2);
|
|
until (Pos2>=End2) or (not IsSpaceChars[fText2[Pos2]]);
|
|
end else begin
|
|
// compare the space chars
|
|
if fText1[Pos1]=fText2[Pos2] then begin
|
|
// no diff -> proceed with next chars
|
|
inc(Pos1);
|
|
inc(Pos2);
|
|
end else begin
|
|
// diff found -> lines differ
|
|
Result:=false;
|
|
exit;
|
|
end;
|
|
end;
|
|
end;
|
|
end;
|
|
end;
|
|
if (Pos1<End1) or (Pos2<End2) then begin
|
|
// one line is longer -> lines differ
|
|
Result:=false;
|
|
exit;
|
|
end;
|
|
// compare line ends
|
|
if not (tdfIgnoreLineEnds in fFlags) then begin
|
|
Pos1:=Line1End;
|
|
Pos2:=Line2End;
|
|
while (Pos1<NextLine1Start) and (Pos2<NextLine2Start)
|
|
and (fText1[Pos1]=fText2[Pos2]) do begin
|
|
inc(Pos1);
|
|
inc(Pos2);
|
|
end;
|
|
Result:=(Pos1=NextLine1Start) and (Pos2=NextLine2Start);
|
|
end else begin
|
|
Result:=true;
|
|
end;
|
|
end;
|
|
|
|
function TDiffOutput.LinesAreEqual(const Line1Extends, Line2Extends: TLineExtends): boolean;
|
|
begin
|
|
Result:=LinesAreEqual(Line1Extends.LineStart, Line1Extends.LineEnd,
|
|
Line1Extends.NextLineStart,
|
|
Line2Extends.LineStart, Line2Extends.LineEnd,
|
|
Line2Extends.NextLineStart);
|
|
end;
|
|
|
|
procedure TDiffOutput.FindNextEqualLine(const Start1, Start2: TLineExtends;
|
|
out EqualLine1, EqualLine2: TLineExtends);
|
|
var
|
|
Max1, Max2, Cur1, Cur2: TLineExtends;
|
|
begin
|
|
Max1:=Start1;
|
|
Max2:=Start2;
|
|
Cur1:=Start1;
|
|
Cur2:=Start2;
|
|
try
|
|
if LinesAreEqual(Cur1,Cur2)
|
|
and (not fPart1.IsEmptyLine(Cur1.LineStart, Cur1.LineEnd)) then
|
|
exit;
|
|
repeat
|
|
// increase Max1
|
|
if GotoNextLine(Max1) then begin
|
|
fPart1.GetLineExtends(Max1);
|
|
// search Max1 Line in fText2
|
|
if Max1.LineStart<Max1.NextLineStart then begin
|
|
Cur1:=Max1;
|
|
Cur2:=Start2;
|
|
repeat
|
|
if LinesAreEqual(Cur1,Cur2)
|
|
and (not fPart1.IsEmptyLine(Cur1.LineStart, Cur1.LineEnd)) then
|
|
exit;
|
|
if Cur2.LineStart>=Max2.LineStart then break;
|
|
Cur2.LineStart:=Cur2.NextLineStart;
|
|
inc(Cur2.LineNumber);
|
|
fPart2.GetLineExtends(Cur2);
|
|
until false;
|
|
end;
|
|
UpdateProgressBar(Max1);
|
|
end;
|
|
// increase Max2
|
|
if GotoNextLine(Max2) then begin
|
|
fPart2.GetLineExtends(Max2);
|
|
// search Max2 Line in fText1
|
|
if Max2.LineStart<Max2.NextLineStart then begin
|
|
Cur1:=Start1;
|
|
Cur2:=Max2;
|
|
repeat
|
|
if LinesAreEqual(Cur1,Cur2)
|
|
and (not fPart1.IsEmptyLine(Cur1.LineStart,Cur1.LineEnd)) then
|
|
exit;
|
|
if Cur1.LineStart>=Max1.LineStart then break;
|
|
Cur1.LineStart:=Cur1.NextLineStart;
|
|
inc(Cur1.LineNumber);
|
|
fPart1.GetLineExtends(Cur1);
|
|
until false;
|
|
end;
|
|
end;
|
|
until (Max1.LineStart>=Max1.NextLineStart)
|
|
and (Max2.LineStart>=Max2.NextLineStart);
|
|
// no equal line found
|
|
Cur1:=Max1;
|
|
Cur2:=Max2;
|
|
finally
|
|
repeat
|
|
EqualLine1:=Cur1;
|
|
EqualLine2:=Cur2;
|
|
// chomp empty lines at end
|
|
fPart1.GetPrevLineExtends(Cur1);
|
|
fPart2.GetPrevLineExtends(Cur2);
|
|
if (Cur1.LineNumber < 1) or (Cur2.LineNumber < 1) then
|
|
break;
|
|
until not LinesAreEqual(Cur1,Cur2);
|
|
end;
|
|
end;
|
|
|
|
function TDiffOutput.CreateTextDiff: string;
|
|
var
|
|
Line1, Line2, EqualLine1, EqualLine2: TLineExtends;
|
|
Len1, Len2: integer;
|
|
begin
|
|
Result := '';
|
|
try
|
|
try
|
|
Len1:=length(fText1);
|
|
Len2:=length(fText2);
|
|
Line1.LineStart:=1;
|
|
Line1.LineNumber:=1;
|
|
Line2.LineStart:=1;
|
|
Line2.LineNumber:=1;
|
|
repeat
|
|
// search for a difference line ...
|
|
repeat
|
|
// skip empty lines in fText1 and get line1 extends ...
|
|
fPart1.GetNextLineExtends(Line1);
|
|
// skip empty lines in fText2 and get line2 extends ...
|
|
fPart2.GetNextLineExtends(Line2);
|
|
// skip equal lines ...
|
|
if (Line1.LineStart<=Len1) and (Line2.LineStart<=Len2) then begin
|
|
if not LinesAreEqual(Line1,Line2) then
|
|
break;
|
|
Line1.LineStart:=Line1.NextLineStart;
|
|
inc(Line1.LineNumber);
|
|
Line2.LineStart:=Line2.NextLineStart;
|
|
inc(Line2.LineNumber);
|
|
end else begin
|
|
if (Line1.LineStart<=Len1) or (Line2.LineStart<=Len2) then begin
|
|
// one text is longer than the other
|
|
AddRestDiff(Line1, Line2);
|
|
end else begin
|
|
// no more diff found
|
|
end;
|
|
exit;
|
|
end;
|
|
UpdateProgressBar(Line1);
|
|
until false;
|
|
// difference line found -> search next equal line
|
|
FindNextEqualLine(Line1, Line2, EqualLine1, EqualLine2);
|
|
AddDiff(Line1, EqualLine1, Line2, EqualLine2);
|
|
// continue the search ...
|
|
Line1:=EqualLine1;
|
|
GotoNextLine(Line1);
|
|
Line2:=EqualLine2;
|
|
GotoNextLine(Line2);
|
|
UpdateProgressBar(Line1);
|
|
until false;
|
|
except
|
|
on E: Exception do begin
|
|
DebugLogger.DebugLn('CreateTextDiff ',E.Message);
|
|
end;
|
|
end;
|
|
finally
|
|
FinishDiff;
|
|
SetLength(Result,fDiffStream.Size);
|
|
fDiffStream.Position:=0;
|
|
if Result<>'' then
|
|
fDiffStream.Read(Result[1],length(Result));
|
|
end;
|
|
end;
|
|
|
|
procedure TDiffOutput.AddRestDiff(const Start1, Start2: TLineExtends);
|
|
var
|
|
End1, End2: TLineExtends;
|
|
begin
|
|
End1.LineStart:=length(fText1)+1;
|
|
End1.LineEnd:=End1.LineStart;
|
|
End1.NextLineStart:=End1.LineStart;
|
|
End1.LineNumber:=Start1.LineNumber+fPart1.CountLinesTillEnd(Start1.LineStart);
|
|
End2.LineStart:=length(fText2)+1;
|
|
End2.LineEnd:=End2.LineStart;
|
|
End2.NextLineStart:=End2.LineStart;
|
|
End2.LineNumber:=Start2.LineNumber+fPart2.CountLinesTillEnd(Start2.LineStart);
|
|
AddDiff(Start1,End1, Start2,End2);
|
|
end;
|
|
|
|
procedure TDiffOutput.AddDiff(const Start1, End1, Start2, End2: TLineExtends);
|
|
begin
|
|
if (Start1.LineStart>length(fText1)) and (Start2.LineStart>length(fText2)) then
|
|
exit; // no diff
|
|
case fOutputType of
|
|
tdoContext:
|
|
AddContextDiff(Start1,End1,Start2,End2);
|
|
else
|
|
AddDefaultDiff(Start1,End1,Start2,End2);
|
|
end;
|
|
end;
|
|
|
|
procedure TDiffOutput.UpdateProgressBar(const Line: TLineExtends);
|
|
begin
|
|
if Assigned(OnProgressPos) then
|
|
OnProgressPos(Line.LineStart);
|
|
end;
|
|
|
|
procedure TDiffOutput.FinishOldContextBlock;
|
|
begin
|
|
if fPart1.fStream <> nil then begin
|
|
fPart1.Write('*** ',' ****');
|
|
fPart2.Write('--- ',' ----');
|
|
end;
|
|
end;
|
|
|
|
procedure TDiffOutput.FinishDiff;
|
|
begin
|
|
case fOutputType of
|
|
tdoContext: FinishOldContextBlock;
|
|
end;
|
|
end;
|
|
|
|
procedure TDiffOutput.AddDefaultDiff(const Start1, End1, Start2, End2: TLineExtends);
|
|
{ Start1/2 is the first line that is different
|
|
End1/2 is the first line that is equal
|
|
|
|
The diff output:
|
|
- There are three types of diffs: insertions, deletions and replacements
|
|
- Insertions:
|
|
- An insertion/addition starts with a
|
|
<lines>a<lines>
|
|
followed by the lines of text 2.
|
|
- A deletion starts with a
|
|
<lines>d<lines>
|
|
followed by the lines of text 1.
|
|
- A replacement/change starts with a
|
|
<lines>c<lines>
|
|
followed by the lines of text 1, folowed by
|
|
---
|
|
followed by the lines of text 2.
|
|
- <lines> can be single decimal number or a range. For example:
|
|
1
|
|
2,3
|
|
- The lines of text 1 are always prefixed with '< '
|
|
If the lines of text 1 do not end with a newline char it ends with a line
|
|
\ No newline at end
|
|
- The lines of text 2 are always prefixed with '> '
|
|
If the lines of text 1 do not end with a newline char it ends with a line
|
|
\ No newline at end
|
|
}
|
|
var
|
|
DiffStartLine1, DiffEndLine1: integer;
|
|
DiffStartLine2, DiffEndLine2: integer;
|
|
ActionChar: char;
|
|
|
|
procedure WriteActionLine;
|
|
begin
|
|
// write line numbers of text 1
|
|
WriteStrToStream(fDiffStream,IntToStr(DiffStartLine1));
|
|
if DiffEndLine1>DiffStartLine1 then begin
|
|
WriteStrToStream(fDiffStream,',');
|
|
WriteStrToStream(fDiffStream,IntToStr(DiffEndLine1));
|
|
end;
|
|
// write action character 'a', 'd' or 'c'
|
|
if (Start1.LineStart<End1.LineStart) then begin
|
|
// part of text 1 is replaced
|
|
if (Start2.LineStart<End2.LineStart) then
|
|
ActionChar:='c' // replacement
|
|
else
|
|
ActionChar:='d'; // deletion
|
|
end else begin
|
|
// insertion
|
|
ActionChar:='a';
|
|
end;
|
|
fDiffStream.Write(ActionChar,1);
|
|
// write line numbers of text 2
|
|
WriteStrToStream(fDiffStream,IntToStr(DiffStartLine2));
|
|
if DiffEndLine2>DiffStartLine2 then begin
|
|
WriteStrToStream(fDiffStream,',');
|
|
WriteStrToStream(fDiffStream,IntToStr(DiffEndLine2));
|
|
end;
|
|
// write <newline>
|
|
WriteStrToStream(fDiffStream,LineBreak);
|
|
end;
|
|
|
|
begin
|
|
DiffStartLine1:=Start1.LineNumber;
|
|
DiffEndLine1:=End1.LineNumber-1;
|
|
if DiffStartLine1>DiffEndLine1 then
|
|
DiffStartLine1:=DiffEndLine1;
|
|
DiffStartLine2:=Start2.LineNumber;
|
|
DiffEndLine2:=End2.LineNumber-1;
|
|
if DiffStartLine2>DiffEndLine2 then
|
|
DiffStartLine2:=DiffEndLine2;
|
|
WriteActionLine;
|
|
fPart1.WriteLinesOfText(fDiffStream,'< ',Start1,End1.LineStart);
|
|
if ActionChar='c' then begin
|
|
WriteStrToStream(fDiffStream,'---');
|
|
WriteStrToStream(fDiffStream,LineBreak);
|
|
end;
|
|
fPart2.WriteLinesOfText(fDiffStream,'> ',Start2,End2.LineStart);
|
|
end;
|
|
|
|
procedure TDiffOutput.AddContextDiff(const Start1, End1, Start2, End2: TLineExtends);
|
|
{ Start1/2 is the first line that is different
|
|
End1/2 is the first line that is equal
|
|
|
|
The diff output:
|
|
- Every diff block starts with 15 stars
|
|
***************
|
|
- Every diff block has two parts. One for each text.
|
|
- The first text part starts with
|
|
*** StartLine,EndLine ****
|
|
- The second text part starts with
|
|
--- StartLine,EndLine ----
|
|
- In front of and behind each changed line there are unchanged line. These
|
|
lines are the context.
|
|
- At the beginning and at the end there are at least
|
|
ContextLineCount number of unchanged lines.
|
|
- Between two changed lines there are at most 2x ContextLineCount number of
|
|
unchanged lines. If there are too many, the a new block is started
|
|
- Changed lines starts with '! ', unchanged with ' '.
|
|
- If a part contains no changed lines, its lines can be left out
|
|
}
|
|
|
|
var
|
|
Part1HasChangedLines: boolean;
|
|
Part2HasChangedLines: boolean;
|
|
begin
|
|
if (fPart1.fStream<>nil)
|
|
and (Start1.LineNumber-ContextLineCount<=fPart1.fEndLine-1)
|
|
and (Start2.LineNumber-ContextLineCount<=fPart2.fEndLine-1) then begin
|
|
// append the new difference
|
|
end else begin
|
|
// start a new block // StartContextBlock(Start1,Start2);
|
|
FinishOldContextBlock;
|
|
WriteStrToStream(fDiffStream,'***************'+LineBreak);
|
|
fPart1.Init(Start1);
|
|
fPart2.Init(Start2);
|
|
end;
|
|
fPart1.fEndLine:=End1.LineNumber+ContextLineCount-1;
|
|
fPart2.fEndLine:=End2.LineNumber+ContextLineCount-1;
|
|
Part1HasChangedLines:=End1.LineStart>Start1.LineStart;
|
|
Part2HasChangedLines:=End2.LineStart>Start2.LineStart;
|
|
fPart1.Write2(Start1,End1,Part2HasChangedLines,'-');
|
|
fPart2.Write2(Start2,End2,Part1HasChangedLines,'+');
|
|
end;
|
|
|
|
function TDiffOutput.GetProgressMax: Integer;
|
|
begin
|
|
Result := Length(fText1); // + Length(fText2);
|
|
end;
|
|
|
|
constructor TDiffOutput.Create(const aText1, aText2: string; aFlags: TTextDiffFlags);
|
|
begin
|
|
fText1:=aText1;
|
|
fText2:=aText2;
|
|
fFlags:=aFlags;
|
|
fOutputType:=tdoContext; // Default OutputType, can be changed later
|
|
fDiffStream:=TMemoryStream.Create;
|
|
fPart1:=TDiffPart.Create(Self, fText1);
|
|
fPart2:=TDiffPart.Create(Self, fText2);
|
|
end;
|
|
|
|
destructor TDiffOutput.Destroy;
|
|
begin
|
|
if Assigned(OnProgressPos) then
|
|
OnProgressPos(0);
|
|
fPart2.Free;
|
|
fPart1.Free;
|
|
fDiffStream.Free;
|
|
inherited Destroy;
|
|
end;
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
procedure InternalInit;
|
|
var
|
|
c: char;
|
|
begin
|
|
for c:=Low(char) to High(char) do begin
|
|
IsSpaceChars[c]:=c in [' ',#9];
|
|
UpperCaseChars[c]:=upcase(c);
|
|
end;
|
|
end;
|
|
|
|
initialization
|
|
InternalInit;
|
|
|
|
finalization
|
|
|
|
|
|
end.
|
|
|