{
 ***************************************************************************
 *                                                                         *
 *   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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.        *
 *                                                                         *
 ***************************************************************************

  Author: Mattias Gaertner

  Abstract:
    Source Editor marks for (compiler) messages.
}
unit etSrcEditMarks;

{$mode objfpc}{$H+}

{off $DEFINE VerboseETSrcChange}

interface

uses
  Classes, SysUtils, math, LazLogger, LazFileUtils, AvgLvlTree,
  KeywordFuncLists, Graphics, Controls, Forms, ImgList,
  SynEditMarkupGutterMark,
  SynEditMarks, SynEditMiscClasses, SynEditTypes, SynEdit, LazSynEditText,
  IDEExternToolIntf;

type

  { TETMarkStyle }

  TETMarkStyle = class
  private
    FColor: TColor;
    FImageIndex: integer;
    FSourceMarkup: TSynSelectedColor;
    FUrgency: TMessageLineUrgency;
    procedure SetColor(AValue: TColor);
  public
    constructor Create(TheUrgency: TMessageLineUrgency; TheColor: TColor);
    destructor Destroy; override;
    property Urgency: TMessageLineUrgency read FUrgency;
    property Color: TColor read FColor write SetColor;
    property ImageIndex: integer read FImageIndex write FImageIndex;
    property SourceMarkup: TSynSelectedColor read FSourceMarkup;
  end;

  TETMarks = class;

  { TETMark }

  TETMark = class(TSynEditMarkupMark)
  private
    FMsgLine: TMessageLine;
    FSourceMarks: TETMarks;
    FUrgency: TMessageLineUrgency;
  public
    destructor Destroy; override;
    property Urgency: TMessageLineUrgency read FUrgency write FUrgency;
    property MsgLine: TMessageLine read FMsgLine write FMsgLine;
    property SourceMarks: TETMarks read FSourceMarks write FSourceMarks;
  end;

  { TLMsgViewLine }

  TLMsgViewLine = class(TMessageLine)
  public
    Mark: TETMark;
    destructor Destroy; override;
  end;

  { TETMarks }

  TOnGetSynEditOfFile = procedure(Sender: TObject; aFilename: string;
    var aSynEdit: TSynEdit) of object;

  TETMarks = class(TComponent)
  private
    FImageList: TCustomImageList;
    fMarkStyles: array[TMessageLineUrgency] of TETMarkStyle;
    FOnGetSynEditOfFile: TOnGetSynEditOfFile;
    FPriority: integer;
    function GetMarkStyles(Urgency: TMessageLineUrgency): TETMarkStyle; inline;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    function CreateMark(MsgLine: TMessageLine; aSynEdit: TSynEdit = nil): TETMark;
    procedure RemoveMarks(aSynEdit: TSynEdit);
    property ImageList: TCustomImageList read FImageList write FImageList; // must have same Width/Height as the TSynEdits bookmarkimages
    property OnGetSynEditOfFile: TOnGetSynEditOfFile read FOnGetSynEditOfFile write FOnGetSynEditOfFile;
    property MarkStyles[Urgency: TMessageLineUrgency]: TETMarkStyle read GetMarkStyles;
    property Priority: integer read FPriority write FPriority;
  end;

var
  ExtToolsMarks: TETMarks = nil;

type
  TETSrcChangeAction = (
    etscaInsert,
    etscaDelete
    );

  { TETSrcChange }

  TETSrcChange = class
  public
    Action: TETSrcChangeAction;
    FromPos: TPoint;
    ToPos: TPoint;
    Prev, Next: TETSrcChange;
    constructor Create(AnAction: TETSrcChangeAction; const aFromPos, aToPos: TPoint);
    constructor Create(AnAction: TETSrcChangeAction; FromPosY, FromPosX, ToPosY, ToPosX: integer);
    function AsString: string;
  end;

  TETMultiSrcChanges = class;

  { TETSingleSrcChanges - edits of single file}

  TETSingleSrcChanges = class
  private
    FFilename: string;
    FFirst: TETSrcChange;
    FLast: TETSrcChange;
    fInPendingTree: boolean;
    FMultiSrcChanges: TETMultiSrcChanges;
    procedure Append(Change: TETSrcChange);
    procedure Remove(Change: TETSrcChange);
    procedure SetFilename(AValue: string);
    procedure SetMultiSrcChanges(AValue: TETMultiSrcChanges);
    procedure SetInPendingTree(AValue: boolean);
  protected
    property InPendingTree: boolean read FInPendingTree write SetInPendingTree;
  public
    constructor Create;
    destructor Destroy; override;
    procedure Clear;
    property MultiSrcChanges: TETMultiSrcChanges read FMultiSrcChanges write SetMultiSrcChanges;
    property First: TETSrcChange read FFirst;
    property Last: TETSrcChange read FLast;
    property Filename: string read FFilename write SetFilename;
    procedure GetRange(out MinY, MaxY, LineDiffBehindMaxY: integer);
    function Add(Action: TETSrcChangeAction; const FromPos, ToPos: TPoint): TETSrcChange; inline;
    function Add(Action: TETSrcChangeAction; FromPosY, FromPosX, ToPosY, ToPosX: integer): TETSrcChange;
    function AdaptCaret(var Line,Col: integer;
      LeftBound: boolean // true = position is bound to character on the left
      ): boolean; // true if changed
    procedure ConsistencyCheck;
    procedure WriteDebugReport(Title: string);
  end;

  { TETMultiSrcChanges - edits of all files }

  TETMultiSrcChanges = class(TComponent)
  private
    fAllChanges: TAvgLvlTree; // tree of TETSingleSrcChanges sorted for Filename
    FAutoSync: boolean;
    fPendingChanges: TAvgLvlTree; // tree of TETSingleSrcChanges sorted for Filename
    FOnSync: TNotifyEvent;
    FSyncQueued: boolean;
    procedure SetSyncQueued(AValue: boolean);
  protected
    procedure DoSync({%H-}Data: PtrInt); // called by Application.QueueAsyncCall
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    function Count: integer; inline;
    procedure Clear;
    function GetChanges(const aFilename: string; CreateIfNotExists: boolean): TETSingleSrcChanges;
    function AdaptCaret(const aFilename: string; var Line,Col: integer;
      LeftBound: boolean // true = position is bound to character on the left
                 ): boolean;
    property AllChanges: TAvgLvlTree read fAllChanges; // tree of TETSingleSrcChanges sorted for Filename
    property PendingChanges: TAvgLvlTree read fPendingChanges; // tree of TETSingleSrcChanges sorted for Filename
    property SyncQueued: boolean read FSyncQueued write SetSyncQueued;
    property OnSync: TNotifyEvent read FOnSync write FOnSync; // called by Application.QueueAsyncCall
    property AutoSync: boolean read FAutoSync write FAutoSync; // true = call OnSync via Application.QueueAsyncCall
  end;

  { TETSynPlugin - create one per file, not one per synedit }

  TIsEnabledEvent = function(Sender: TObject): boolean of object;

  TETSynPlugin = class(TLazSynEditPlugin)
  private
    FChanges: TETSingleSrcChanges;
    FEnabled: boolean;
    FOnIsEnabled: TIsEnabledEvent;
  protected
    procedure OnLineEdit(Sender: TSynEditStrings; aLinePos, aBytePos, aCount,
      aLineBrkCnt: Integer; {%H-}aText: String);
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    property Changes: TETSingleSrcChanges read FChanges write FChanges;
    property Enabled: boolean read FEnabled write FEnabled;
    property OnIsEnabled: TIsEnabledEvent read FOnIsEnabled write FOnIsEnabled;
  end;

function IsCaretInFront(Line1, Col1, Line2, Col2: integer): boolean; inline; overload;
function IsCaretInFront(const P1: TPoint; Line2, Col2: integer): boolean; inline; overload;
function IsCaretInFront(Line1, Col1: integer; const P2: TPoint): boolean; inline; overload;
function IsCaretInFront(const P1,P2: TPoint): boolean; inline; overload;
function IsCaretInFrontOrSame(Line1, Col1, Line2, Col2: integer): boolean; inline; overload;
function IsCaretInFrontOrSame(const P1: TPoint; Line2, Col2: integer): boolean; inline; overload;
function IsCaretInFrontOrSame(Line1, Col1: integer; const P2: TPoint): boolean; inline; overload;
function IsCaretInFrontOrSame(const P1,P2: TPoint): boolean; inline; overload;
procedure AdaptCaret(var Line,Col: integer;
  LeftBound: boolean; // true = position is bound to character on the left
  Action: TETSrcChangeAction;
  FromPosY, FromPosX, ToPosY, ToPosX: integer);

function CompareETSrcChangesFilenames(Changes1, Changes2: Pointer): integer;
function CompareFilenameAndETSrcChanges(aFilenameStr, Changes: Pointer): integer;

function dbgs(Action: TETSrcChangeAction): string; overload;
procedure Test_AdaptCaret;
procedure Test_MergeTETSrcChanges;

implementation

function IsCaretInFront(Line1, Col1, Line2, Col2: integer): boolean;
begin
  Result:=(Line1<Line2) or ((Line1=Line2) and (Col1<Col2));
end;

function IsCaretInFront(const P1: TPoint; Line2, Col2: integer): boolean;
begin
  Result:=IsCaretInFront(P1.Y,P1.X,Line2,Col2);
end;

function IsCaretInFront(Line1, Col1: integer; const P2: TPoint): boolean;
begin
  Result:=IsCaretInFront(Line1,Col1,P2.Y,P2.X);
end;

function IsCaretInFront(const P1, P2: TPoint): boolean;
begin
  Result:=IsCaretInFront(P1.Y,P1.X,P2.Y,P2.X);
end;

function IsCaretInFrontOrSame(Line1, Col1, Line2, Col2: integer): boolean;
begin
  Result:=(Line1<Line2) or ((Line1=Line2) and (Col1<=Col2));
end;

function IsCaretInFrontOrSame(const P1: TPoint; Line2, Col2: integer): boolean;
begin
  Result:=IsCaretInFrontOrSame(P1.Y,P1.X,Line2,Col2);
end;

function IsCaretInFrontOrSame(Line1, Col1: integer; const P2: TPoint): boolean;
begin
  Result:=IsCaretInFrontOrSame(Line1,Col1,P2.Y,P2.X);
end;

function IsCaretInFrontOrSame(const P1, P2: TPoint): boolean;
begin
  Result:=IsCaretInFrontOrSame(P1.Y,P1.X,P2.Y,P2.X);
end;

procedure AdaptCaret(var Line, Col: integer; LeftBound: boolean;
  Action: TETSrcChangeAction; FromPosY, FromPosX, ToPosY, ToPosX: integer);
begin
  //debugln(['AdaptCaret Line=',Line,' Col=',Col,' LeftBound=',LeftBound,' Action=',dbgs(Action),' FromPos=',FromPosY,',',FromPosX,' ToPos=',ToPosY,',',ToPosX]);
  if Line<FromPosY then exit;
  if Action=etscaInsert then begin
    // insert
    if Line>FromPosY then begin
      // insert in lines in front => move vertically
      inc(Line,ToPosY-FromPosY);
    end else begin
      // insert in same line
      if LeftBound then begin
        if Col<=FromPosX then exit;
      end else begin
        if Col<FromPosX then exit;
      end;
      if FromPosY<ToPosY then begin
        // multi line insert
        inc(Line,ToPosY-FromPosY);
        Col:=ToPosX+Col-FromPosX;
      end else begin
        // inserting some characters
        inc(Col,ToPosX-FromPosX);
      end;
    end;
  end else begin
    // delete
    if Line>ToPosY then begin
      // delete some lines in front => move vertically
      dec(Line,ToPosY-FromPosY);
    end else if Line<ToPosY then begin
      if Line>FromPosY then begin
        // whole line of position was deleted => move to start of deletion
        Line:=FromPosY;
        Col:=FromPosX;
      end else begin
        // Line=FromPosY, Line<ToPosY
        if Col<=FromPosX then begin
          // delete is behind position => ignore
        end else begin
          // position was deleted => move to start of deletion
          Line:=FromPosY;
          Col:=FromPosX;
        end;
      end;
    end else begin
      // Line=ToPosY
      if Line>FromPosY then begin
        // multi line delete
        if Col<=ToPosX then begin
          // position was deleted => move to start of deletion
          Line:=FromPosY;
          Col:=FromPosX;
        end else begin
          // some characters at the start of the line were deleted
          Line:=FromPosY;
          dec(Col,ToPosX-1);
        end;
      end else begin
        // Line=FromPosY=ToPosY
        if Col<=FromPosX then begin
          // delete is behind position => ignore
        end else if Col<=ToPosX then begin
          // position was deleted => move to start of deletion
          Col:=FromPosX;
        end else begin
          // some characters in front were deleted
          dec(Col,ToPosX-FromPosX);
        end;
      end;
    end;
  end;
  //debugln(['AdaptCaret ',Line,',',Col]);
end;

function CompareETSrcChangesFilenames(Changes1, Changes2: Pointer): integer;
var
  SrcChanges1: TETSingleSrcChanges absolute Changes1;
  SrcChanges2: TETSingleSrcChanges absolute Changes2;
begin
  Result:=CompareFilenames(SrcChanges1.Filename,SrcChanges2.Filename);
end;

function CompareFilenameAndETSrcChanges(aFilenameStr, Changes: Pointer
  ): integer;
var
  SrcChanges: TETSingleSrcChanges absolute Changes;
begin
  Result:=CompareFilenames(AnsiString(aFilenameStr),SrcChanges.Filename);
end;

function dbgs(Action: TETSrcChangeAction): string;
begin
  Result:='';
  WriteStr(Result,Action);
end;

procedure Test_AdaptCaret;

  procedure T(Title: string; Line,Col: integer;
    LeftBound: boolean; // true = position is bound to character on the left
    Action: TETSrcChangeAction;
    FromPosY, FromPosX, ToPosY, ToPosX: integer;
    ExpectedLine, ExpectedCol: integer);
  var
    Y: Integer;
    X: Integer;
    s: String;
  begin
    Y:=Line;
    X:=Col;
    AdaptCaret(Y,X,LeftBound,Action,FromPosY,FromPosX,ToPosY,ToPosX);
    if (Y=ExpectedLine) and (X=ExpectedCol) then exit;
    s:='Test_AdaptCaret: Caret='+dbgs(Line)+','+dbgs(Col)
     +' LeftBound='+dbgs(LeftBound)
     +' Action='+dbgs(Action)
     +' FromPos='+dbgs(FromPosY)+','+dbgs(FromPosX)
     +' ToPos='+dbgs(ToPosY)+','+dbgs(ToPosX)
     +' Expected='+dbgs(ExpectedLine)+','+dbgs(ExpectedCol)
     +' Actual='+dbgs(Y)+','+dbgs(X);
    raise Exception.Create(Title+': '+s);
  end;

begin
  T('Insert chars in front',10,10,true,etscaInsert,1,1, 1,2,  10,10);
  T('Insert lines in front',10,10,true,etscaInsert,1,1, 2,2,  11,10);
  T('Insert chars behind',10,10,true,etscaInsert,12,1, 12,2,  10,10);
  T('Insert chars in front, same line',10,10,true,etscaInsert,10,1, 10,2,  10,11);
  T('Insert chars in front, same line',10,40,true,etscaInsert,10,28, 10,29,  10,41);
  T('Insert chars behind, same line',10,10,true,etscaInsert,10,11, 10,12,  10,10);
  T('Insert chars behind, same line, leftbound',10,10,true,etscaInsert,10,10, 10,12,  10,10);
  T('Insert chars behind, same line, rightbound',10,10,false,etscaInsert,10,10, 10,12,  10,12);
  T('Insert chars and line breaks in front, same line',10,10,true,etscaInsert,10,1, 11,1,  11,10);
  T('Insert chars and line breaks in front, same line',10,10,true,etscaInsert,10,1, 11,2,  11,11);
  T('Insert chars and line breaks in front, same line',10,10,true,etscaInsert,10,2, 11,2,  11,10);
  T('Insert chars and line breaks in front, same line',10,10,true,etscaInsert,10,2, 11,5,  11,13);
  T('Insert chars and line breaks in front, same line',10,10,true,etscaInsert,10,2, 13,5,  13,13);

  T('Delete chars in front',10,10,true,etscaDelete, 1,1, 1,2, 10,10);
  T('Delete lines in front',10,10,true,etscaDelete, 1,1, 2,2, 9,10);
  T('Delete chars in front, same line',10,10,true,etscaDelete, 10,1, 10,2, 10,9);
  T('Delete lines behind',10,10,true,etscaDelete, 11,1, 12,2, 10,10);
  T('Delete chars behind, same line',10,10,true,etscaDelete, 10,11, 10,12, 10,10);
  T('Delete chars behind, same line',10,10,true,etscaDelete, 10,10, 10,12, 10,10);
  T('Delete lines in front, same line',10,10,true,etscaDelete, 9,1, 10,1, 9,10);
  T('Delete lines in front, same line',10,10,true,etscaDelete, 9,1, 10,3, 9,8);
  T('Delete position',10,10,true,etscaDelete, 9,1, 11,1, 9,1);
  T('Delete position',10,10,true,etscaDelete, 10,1, 11,1, 10,1);
  T('Delete position',10,10,true,etscaDelete, 10,5, 10,11, 10,5);
end;

procedure Test_MergeTETSrcChanges;
var
  Changes: TETSingleSrcChanges;

  procedure Check(Title: string; aChanges: array of TETSrcChange);

    procedure E(Msg: string);
    var
      s: String;
    begin
      s:=Title+', '+Msg;
      Changes.WriteDebugReport(s);
      raise Exception.Create(s);
    end;

  var
    i: Integer;
    ActualChange: TETSrcChange;
    ExpectedChange: TETSrcChange;
  begin
    ActualChange:=Changes.First;
    try
      for i:=Low(aChanges) to High(aChanges) do begin
        ExpectedChange:=aChanges[i];
        if ExpectedChange=nil then begin
          if ActualChange<>nil then
            E('too many changes');
          exit;
        end;
        if ActualChange=nil then
          E('not enough changes (missing: '+ActualChange.AsString+')');
        if ExpectedChange.AsString<>ActualChange.AsString then
          E('diff: Expected=('+ExpectedChange.AsString+'), Actual=('+ActualChange.AsString+')');
        ActualChange:=ActualChange.Next;
      end;
    finally
      for i:=Low(aChanges) to High(aChanges) do
        aChanges[i].Free;
    end;
  end;

begin
  Changes:=TETSingleSrcChanges.Create;
  try
    Changes.ConsistencyCheck;

    // test empty clear
    Changes.Clear;
    Changes.ConsistencyCheck;

    // test merge insert
    Changes.Add(etscaInsert,1,1,1,46);
    Changes.ConsistencyCheck;
    Changes.Add(etscaInsert,1,46,2,1);
    Changes.ConsistencyCheck;
    Check('Merge insert',[TETSrcChange.Create(etscaInsert,1,1,2,1)]);
    Changes.Clear;

    // insert characters into a previous multi line insert
    Changes.Add(etscaInsert,10,1,12,1);
    Changes.ConsistencyCheck;
    Changes.Add(etscaInsert,10,1,10,2);
    Changes.ConsistencyCheck;
    Check('Ignore small insert',[TETSrcChange.Create(etscaInsert,10,1,12,1)]);
    Changes.Clear;

    // delete behind previous delete
    Changes.Add(etscaDelete,1,2,1,4);
    Changes.ConsistencyCheck;
    Changes.Add(etscaDelete,1,2,1,5);
    Changes.ConsistencyCheck;
    Check('combine deleting characters',[TETSrcChange.Create(etscaDelete,1,2,1,7)]);
    Changes.Clear;

    // delete encloses a previous delete
    Changes.Add(etscaDelete,1,2,1,4);
    Changes.ConsistencyCheck;
    Changes.Add(etscaDelete,1,1,1,5);
    Changes.ConsistencyCheck;
    Check('combine deleting characters',[TETSrcChange.Create(etscaDelete,1,1,1,7)]);
    Changes.Clear;

    // delete in front of a previous delete
    Changes.Add(etscaDelete,2,2,2,4);
    Changes.ConsistencyCheck;
    Changes.Add(etscaDelete,1,1,2,2);
    Changes.ConsistencyCheck;
    Check('combine deleting characters',[TETSrcChange.Create(etscaDelete,1,1,2,4)]);
    Changes.Clear;

    // delete encloses a previous delete of characters
    Changes.Add(etscaDelete,2,2,2,4);
    Changes.ConsistencyCheck;
    Changes.Add(etscaDelete,1,1,3,1);
    Changes.ConsistencyCheck;
    Check('combine deleting characters',[TETSrcChange.Create(etscaDelete,1,1,3,1)]);
    Changes.Clear;

    // delete encloses a previous delete of a line
    Changes.Add(etscaDelete,2,2,3,4);
    Changes.ConsistencyCheck;
    Changes.Add(etscaDelete,1,1,4,1);
    Changes.ConsistencyCheck;
    Check('combine deleting characters',[TETSrcChange.Create(etscaDelete,1,1,5,1)]);
    Changes.Clear;

    // delete encloses a previous delete at end
    Changes.Add(etscaDelete,2,2,3,1);
    Changes.ConsistencyCheck;
    Changes.Add(etscaDelete,1,1,2,3);
    Changes.ConsistencyCheck;
    Check('combine deleting characters',[TETSrcChange.Create(etscaDelete,1,1,3,2)]);
    Changes.Clear;

    // delete encloses a previous delete at end
    Changes.Add(etscaDelete,2,2,3,2);
    Changes.ConsistencyCheck;
    Changes.Add(etscaDelete,1,1,2,3);
    Changes.ConsistencyCheck;
    Check('combine deleting characters',[TETSrcChange.Create(etscaDelete,1,1,3,3)]);
    Changes.Clear;

    // delete encloses a previous delete at start
    Changes.Add(etscaDelete,1,2,3,2);
    Changes.ConsistencyCheck;
    Changes.Add(etscaDelete,1,1,1,2);
    Changes.ConsistencyCheck;
    Check('combine deleting characters',[TETSrcChange.Create(etscaDelete,1,1,3,2)]);
    Changes.Clear;
  finally
    Changes.Free;
  end;
end;

{ TETMultiSrcChanges }

// inline
function TETMultiSrcChanges.Count: integer;
begin
  Result:=fAllChanges.Count;
end;

procedure TETMultiSrcChanges.SetSyncQueued(AValue: boolean);
begin
  //debugln(['TETMultiSrcChanges.SetSyncQueued ',AValue]);
  if csDesigning in ComponentState then
    AValue:=false;
  if FSyncQueued=AValue then Exit;
  FSyncQueued:=AValue;
  if SyncQueued then
    Application.QueueAsyncCall(@DoSync,0)
  else
    Application.RemoveAsyncCalls(Self);
end;

procedure TETMultiSrcChanges.DoSync(Data: PtrInt);
var
  Node: TAvgLvlTreeNode;
  NextNode: TAvgLvlTreeNode;
begin
  //debugln(['TETMultiSrcChanges.DoSync Files=',fPendingChanges.Count]);
  FSyncQueued:=false;
  if fPendingChanges.Count=0 then exit;
  if Assigned(OnSync) then
    OnSync(Self);
  // clear pending
  Node:=fPendingChanges.FindLowest;
  while Node<>nil do begin
    NextNode:=Node.Successor;
    TETSingleSrcChanges(Node.Data).Clear; // this removes Node from fPendingChanges
    Node:=NextNode;
  end;
  fPendingChanges.Clear;
  //debugln(['TETMultiSrcChanges.DoSync END FSyncQueued=',FSyncQueued,' fPendingChanges=',fPendingChanges.Count]);
end;

constructor TETMultiSrcChanges.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  fAllChanges:=TAvgLvlTree.Create(@CompareETSrcChangesFilenames);
  fPendingChanges:=TAvgLvlTree.Create(@CompareETSrcChangesFilenames);
end;

destructor TETMultiSrcChanges.Destroy;
begin
  SyncQueued:=false;
  Clear;
  FreeAndNil(fPendingChanges);
  FreeAndNil(fAllChanges);
  inherited Destroy;
end;

procedure TETMultiSrcChanges.Clear;
begin
  SyncQueued:=false;
  fPendingChanges.Clear;
  fAllChanges.FreeAndClear;
end;

function TETMultiSrcChanges.GetChanges(const aFilename: string;
  CreateIfNotExists: boolean): TETSingleSrcChanges;
var
  Node: TAvgLvlTreeNode;
begin
  Node:=fAllChanges.FindKey(Pointer(aFilename),@CompareFilenameAndETSrcChanges);
  if Node<>nil then
    Result:=TETSingleSrcChanges(Node.Data)
  else if CreateIfNotExists then begin
    Result:=TETSingleSrcChanges.Create;
    Result.Filename:=aFilename;
    Result.MultiSrcChanges:=Self;
  end else
    Result:=nil;
end;

function TETMultiSrcChanges.AdaptCaret(const aFilename: string; var Line,
  Col: integer; LeftBound: boolean): boolean;
var
  Changes: TETSingleSrcChanges;
begin
  Changes:=GetChanges(aFilename,false);
  if Changes=nil then
    Result:=false
  else
    Result:=Changes.AdaptCaret(Line,Col,LeftBound);
end;

{ TETSrcChange }

constructor TETSrcChange.Create(AnAction: TETSrcChangeAction; const aFromPos,
  aToPos: TPoint);
begin
  Action:=AnAction;
  FromPos:=aFromPos;
  ToPos:=aToPos;
end;

constructor TETSrcChange.Create(AnAction: TETSrcChangeAction; FromPosY,
  FromPosX, ToPosY, ToPosX: integer);
begin
  Action:=AnAction;
  FromPos.Y:=FromPosY;
  FromPos.X:=FromPosX;
  ToPos.Y:=ToPosY;
  ToPos.X:=ToPosX;
end;

function TETSrcChange.AsString: string;
begin
  if Action=etscaInsert then
    Result:='Insert'
  else
    Result:='Delete';
  Result+='-From='+IntToStr(FromPos.Y)+','+IntToStr(FromPos.X);
  Result+='-To='+IntToStr(ToPos.Y)+','+IntToStr(ToPos.X);
end;

{ TETSingleSrcChanges }

procedure TETSingleSrcChanges.SetInPendingTree(AValue: boolean);
begin
  if FMultiSrcChanges=nil then AValue:=false;
  if FInPendingTree=AValue then Exit;
  FInPendingTree:=AValue;
  if fInPendingTree then begin
    FMultiSrcChanges.fPendingChanges.Add(Self);
    if FMultiSrcChanges.AutoSync then
      FMultiSrcChanges.SyncQueued:=true;
  end else
    FMultiSrcChanges.fPendingChanges.Remove(Self);
end;

procedure TETSingleSrcChanges.Append(Change: TETSrcChange);
begin
  if First=nil then begin
    FFirst:=Change;
    InPendingTree:=true;
  end else begin
    FLast.Next:=Change;
    Change.Prev:=Last;
  end;
  fLast:=Change;
end;

procedure TETSingleSrcChanges.Remove(Change: TETSrcChange);
begin
  if First=Change then begin
    FFirst:=Change.Next;
    if FFirst=nil then
      InPendingTree:=false;
  end;
  if Last=Change then
    fLast:=Change.Prev;
  if Change.Prev<>nil then
    Change.Prev.Next:=Change.Next;
  if Change.Next<>nil then
    Change.Next.Prev:=Change.Prev;
  Change.Prev:=nil;
  Change.Next:=nil;
end;

procedure TETSingleSrcChanges.SetFilename(AValue: string);
var
  HasChanged: Boolean;
begin
  if FFilename=AValue then Exit;
  HasChanged:=CompareFilenames(FFilename,AValue)<>0;
  if HasChanged then begin
    if FMultiSrcChanges<>nil then
      raise Exception.Create('TETSingleSrcChanges.SetFilename');
  end;
  FFilename:=AValue;
  if HasChanged then
    Clear;
end;

procedure TETSingleSrcChanges.SetMultiSrcChanges(AValue: TETMultiSrcChanges);
begin
  if FMultiSrcChanges=AValue then Exit;
  if Filename='' then
    raise Exception.Create('TETSingleSrcChanges.SetMultiSrcChanges empty filename');
  if (FMultiSrcChanges<>nil) then begin
    if (csDestroying in FMultiSrcChanges.ComponentState) then begin
      fInPendingTree:=false;
    end else begin
      InPendingTree:=false;
      FMultiSrcChanges.fAllChanges.Remove(Self);
    end;
  end;
  FMultiSrcChanges:=AValue;
  if FMultiSrcChanges<>nil then begin
    FMultiSrcChanges.fAllChanges.Add(Self);
    InPendingTree:=First<>nil;
  end;
end;

constructor TETSingleSrcChanges.Create;
begin
end;

destructor TETSingleSrcChanges.Destroy;
begin
  MultiSrcChanges:=nil;
  Clear;
  inherited Destroy;
end;

procedure TETSingleSrcChanges.Clear;
var
  Item: TETSrcChange;
  CurItem: TETSrcChange;
begin
  Item:=First;
  while Item<>nil do begin
    CurItem:=Item;
    Item:=Item.Next;
    CurItem.Free;
  end;
  fFirst:=nil;
  FLast:=nil;
  InPendingTree:=false;
end;

procedure TETSingleSrcChanges.GetRange(out MinY, MaxY, LineDiffBehindMaxY: integer);
// true if there are changes
// All changes were done between lines MinY and MaxY (inclusive).
// Lines behind MaxY are moved by LineDiffBehindMaxY.
// In other words:
//   if MinY<=Line<=MaxY then AdaptCaret(Line,Col,...)
//   else if Line>MaxY then inc(Line,LineDiffBehindMaxY);
var
  Change: TETSrcChange;
  y: Integer;
  x: Integer;
begin
  MinY:=High(Integer);
  MaxY:=0;
  LineDiffBehindMaxY:=0;
  Change:=First;
  if Change=nil then exit;
  while Change<>nil do begin
    MinY:=Min(MinY,Change.FromPos.Y);
    if Change.Action=etscaInsert then
      MaxY:=Max(MaxY,Change.FromPos.Y)
    else
      MaxY:=Max(MaxY,Change.ToPos.Y);
    Change:=Change.Next;
  end;
  y:=MaxY+1;
  x:=1;
  AdaptCaret(y,x,true);
  LineDiffBehindMaxY:=y-(MaxY+1);
end;

// inline
function TETSingleSrcChanges.Add(Action: TETSrcChangeAction; const FromPos,
  ToPos: TPoint): TETSrcChange;
begin
  Result:=Add(Action,FromPos.Y,FromPos.X,ToPos.Y,ToPos.X);
end;

function TETSingleSrcChanges.Add(Action: TETSrcChangeAction; FromPosY, FromPosX,
  ToPosY, ToPosX: integer): TETSrcChange;

  procedure RaiseFromPosBehindToPos;
  begin
    raise Exception.CreateFmt('TETSrcChanges.Add FromPos=%s,%s behind ToPos=%s,%s',[FromPosY,FromPosX,ToPosY,ToPosX]);
  end;

  function Merge(Prev, Cur: TETSrcChange): boolean;
  begin
    if (Prev=nil) or (Prev.Action<>Action) then
      exit(false);
    // check if addition can be merged
    if Action=etscaInsert then begin
      // Insertion
      if (Prev.ToPos.Y=Cur.FromPos.Y) and (Prev.ToPos.X=Cur.FromPos.X) then begin
        // Cur is an insert exactly behind Prev insert -> append insert
        Prev.ToPos.Y:=Cur.ToPos.Y;
        Prev.ToPos.X:=Cur.ToPos.X;
        {$IFDEF VerboseETSrcChange}
        debugln(['TETSrcChanges.Add appending insert: ',Prev.AsString]);
        {$ENDIF}
        exit(true);
      end;
      if (Cur.FromPos.Y=Cur.ToPos.Y)
      and (Prev.FromPos.Y<=Cur.FromPos.Y) and (Prev.ToPos.Y>Cur.FromPos.Y) then begin
        // Cur inserts characters into a Prev multi line insert -> ignore
        {$IFDEF VerboseETSrcChange}
        debugln(['TETSrcChanges.Add inserting characters into a multi line insert -> ignore']);
        {$ENDIF}
        exit(true);
      end;
      // ToDo: insert exactly in front
    end else begin
      // Deletion
      if IsCaretInFrontOrSame(Cur.FromPos,Prev.FromPos)
      and IsCaretInFrontOrSame(Prev.FromPos,Cur.ToPos) then begin
        // Cur delete extends Prev delete => combine delete
        etSrcEditMarks.AdaptCaret(Cur.ToPos.Y,Cur.ToPos.X,false,etscaInsert,
          Prev.FromPos.Y,Prev.FromPos.X,Prev.ToPos.Y,Prev.ToPos.X);
        Prev.ToPos:=Cur.ToPos;
        Prev.FromPos:=Cur.FromPos;
        {$IFDEF VerboseETSrcChange}
        debugln(['TETSrcChanges.Add delete encloses previous delete: ',Prev.AsString]);
        {$ENDIF}
        exit(true);
      end;
    end;
    Result:=false;
  end;

begin
  {$IFDEF VerboseETSrcChange}
  debugln(['TETSrcChanges.Add Action=',dbgs(Action),' From=',FromPosY,',',FromPosX,' To=',ToPosY,',',ToPosX]);
  {$ENDIF}

  if (FromPosY=ToPosY) and (FromPosX=ToPosX) then
    exit(nil); // no change => ignore

  // consistency check
  if IsCaretInFront(ToPosY,ToPosX,FromPosY,FromPosX) then
    RaiseFromPosBehindToPos;

  Result:=TETSrcChange.Create(Action, FromPosY, FromPosX, ToPosY, ToPosX);

  if Merge(Last,Result) then begin
    repeat
      Result.Free;
      Result:=Last;
      if not Merge(Result.Prev,Result) then exit;
      Remove(Last);
    until false;
  end else begin
    Append(Result);
  end;
end;

function TETSingleSrcChanges.AdaptCaret(var Line, Col: integer; LeftBound: boolean
  ): boolean;
var
  Change: TETSrcChange;
  OldCol: Integer;
  OldLine: Integer;
begin
  OldCol:=Col;
  OldLine:=Line;
  Change:=First;
  while Change<>nil do begin
    etSrcEditMarks.AdaptCaret(Line,Col,LeftBound,Change.Action,
      Change.FromPos.Y,Change.FromPos.X,Change.ToPos.Y,Change.ToPos.X);
    Change:=Change.Next;
  end;
  Result:=(Line<>OldLine) or (Col<>OldCol);
end;

procedure TETSingleSrcChanges.ConsistencyCheck;

  procedure E(Msg: string);
  begin
    raise Exception.Create('TETSrcChanges ConsistencyError: '+Msg);
  end;

var
  Change: TETSrcChange;
  List: TFPList;
  ReallyInPendingTree: Boolean;
begin
  if (First=nil)<>(Last=nil) then
    E('(First=nil)<>(Last=nil)');
  List:=TFPList.Create;
  try
    Change:=First;
    while Change<>nil do begin
      if IsCaretInFront(Change.ToPos,Change.FromPos) then
        E('FromPos>ToPos: '+Change.AsString);
      if Change.Prev<>nil then begin
        if Change.Prev.Next<>Change then
          E('Change.Prev.Next<>Change '+Change.AsString);
      end else begin
        if Change<>First then
          E('Change.Prev=nil');
      end;
      if (Change.Next=nil) and (Change<>Last) then
        E('Change.Next=nil');
      if List.IndexOf(Change)>=0 then
        E('Cycle '+Change.AsString);
      List.Add(Change);
      Change:=Change.Next;
    end;
  finally
    List.Free;
  end;
  if MultiSrcChanges<>nil then begin
    if MultiSrcChanges.fAllChanges.Find(Self)=nil then
      E('MultiSrcChanges.fAllChanges.Find(Self)=nil');
    ReallyInPendingTree:=MultiSrcChanges.fPendingChanges.Find(Self)<>nil;
    if InPendingTree<>ReallyInPendingTree then
      E('InPendingTree<>ReallyInPendingTree');
  end else begin
    if InPendingTree then
      E('MultiSrcChanges=nil InPendingTree=true');
  end;
end;

procedure TETSingleSrcChanges.WriteDebugReport(Title: string);
var
  Change: TETSrcChange;
begin
  debugln('TETSrcChanges.WriteDebugReport ',Title);
  Change:=First;
  while Change<>nil do begin
    debugln('  ',Change.AsString);
    Change:=Change.Next;
  end;
end;

{ TETSynPlugin }

procedure TETSynPlugin.OnLineEdit(Sender: TSynEditStrings; aLinePos, aBytePos,
  aCount, aLineBrkCnt: Integer; aText: String);
{
  aLinePos is 1-based
  aBytePos is 1-based column in line

  Insert:
    aCount > 0
  Delete:
    aCount < 0
    Example deleting line 290..292:
      LinePos=291 BytePos=1 Count=-45 LineBrkCnt=0 Text=""
      LinePos=292 BytePos=1 Count=-33 LineBrkCnt=0 Text=""
      LinePos=291 BytePos=1 Count=0 LineBrkCnt=-2 Text=""
      LinePos=290 BytePos=70 Count=0 LineBrkCnt=-1 Text=""
      LinePos=290 BytePos=1 Count=-69 LineBrkCnt=0 Text=""
}
begin
  if not Enabled then exit;
  if Changes=nil then exit;
  if Assigned(OnIsEnabled) and not OnIsEnabled(Self) then exit;

  {$IFDEF VerboseETSrcChange}
  debugln(['TETSynPlugin.OnLineEdit LinePos=',aLinePos,' BytePos=',aBytePos,' Count=',aCount,' LineBrkCnt=',aLineBrkCnt,' Text="',dbgstr(aText),'"']);
  {$ENDIF}
  if aCount>0 then begin
    // insert characters
    FChanges.Add(etscaInsert,aLinePos,aBytePos,aLinePos,aBytePos+aCount);
  end else if aCount<0 then begin
    // delete characters
    FChanges.Add(etscaDelete,aLinePos,aBytePos,aLinePos,aBytePos-aCount);
  end else if aLineBrkCnt>0 then begin
    // insert line breaks
    // Note: always at end of line, because Count=0
    FChanges.Add(etscaInsert,aLinePos,aBytePos,aLinePos+aLineBrkCnt,1);
  end else if aLineBrkCnt<0 then begin
    // delete line breaks / empty lines
    FChanges.Add(etscaDelete,aLinePos,aBytePos,aLinePos-aLineBrkCnt,1);
  end;
end;

constructor TETSynPlugin.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  ViewedTextBuffer.AddEditHandler(@OnLineEdit);
  FEnabled:=true;
end;

destructor TETSynPlugin.Destroy;
begin
  ViewedTextBuffer.RemoveEditHandler(@OnLineEdit);
  inherited Destroy;
end;

{ TETMark }

destructor TETMark.Destroy;
begin
  if MsgLine is TLMsgViewLine then
    TLMsgViewLine(MsgLine).Mark:=nil;
  MsgLine:=nil;
  inherited Destroy;
end;

{ TETMarks }

// inline
function TETMarks.GetMarkStyles(Urgency: TMessageLineUrgency): TETMarkStyle;
begin
  Result:=fMarkStyles[Urgency];
end;

constructor TETMarks.Create(AOwner: TComponent);
const
  DefMarkColorHint = TColor($00a5ff);
  DefMarkColorError = clRed;
var
  u: TMessageLineUrgency;
begin
  inherited Create(AOwner);
  if ExtToolsMarks=nil then
    ExtToolsMarks:=Self;
  for u:=low(TMessageLineUrgency) to high(TMessageLineUrgency) do
    fMarkStyles[u]:=TETMarkStyle.Create(u,DefMarkColorHint);
  fMarkStyles[mluWarning].Color:=DefMarkColorError;
  fMarkStyles[mluError].Color:=DefMarkColorError;
  fMarkStyles[mluFatal].Color:=DefMarkColorError;
  fMarkStyles[mluPanic].Color:=DefMarkColorError;
end;

destructor TETMarks.Destroy;
var
  u: TMessageLineUrgency;
begin
  if ExtToolsMarks=Self then
    ExtToolsMarks:=nil;
  for u:=low(TMessageLineUrgency) to high(TMessageLineUrgency) do
    FreeAndNil(fMarkStyles[u]);
  inherited Destroy;
end;

function TETMarks.CreateMark(MsgLine: TMessageLine; aSynEdit: TSynEdit
  ): TETMark;
var
  Line: Integer;
  Column: Integer;
  LineSrc: String;
begin
  Result:=nil;
  if (MsgLine.Line<1) or (MsgLine.Column<1) or (MsgLine.Filename='') then exit;
  if aSynEdit=nil then begin
    if OnGetSynEditOfFile=nil then exit;
    OnGetSynEditOfFile(Self,MsgLine.Filename,aSynEdit);
    if (aSynEdit=nil) then exit;
  end;
  Line:=MsgLine.Line;
  Column:=MsgLine.Column;
  if (mlfLeftToken in MsgLine.Flags) then begin
    // the mark is at the of the token
    // synedit only supports starts of tokens
    // => adjust to start of token
    if (Column>1) and (Line>=1) and (Line<=aSynEdit.Lines.Count) then begin
      LineSrc:=aSynEdit.Lines[Line-1];
      if (Column<=length(LineSrc)+1) and (not IsSpaceChar[LineSrc[Column-1]])
      then begin
        dec(Column);
        if IsIdentChar[LineSrc[Column]] then begin
          while (Column>1) and (IsIdentChar[LineSrc[Column-1]]) do
            dec(Column);
        end;
      end;
    end;
  end;
  Result:=TETMark.Create(aSynEdit);
  Result.SourceMarks:=Self;
  Result.MsgLine:=MsgLine;
  Result.Line:=Line;
  Result.Column:=Column;
  Result.Visible:=true;
  Result.Priority:=Priority;
  Result.Urgency:=MsgLine.Urgency;
  Result.ImageList:=ImageList;
  Result.ImageIndex:=MarkStyles[Result.Urgency].ImageIndex;
  Result.SourceMarkup:=MarkStyles[Result.Urgency].SourceMarkup;
  aSynEdit.Marks.Add(Result);
end;

procedure TETMarks.RemoveMarks(aSynEdit: TSynEdit);
var
  i: Integer;
  Mark: TSynEditMark;
begin
  for i:=aSynEdit.Marks.Count-1 downto 0 do begin
    Mark:=aSynEdit.Marks[i];
    if Mark is TETMark then
      Mark.Free;
  end;
end;

{ TETMarkStyle }

procedure TETMarkStyle.SetColor(AValue: TColor);
begin
  if FColor=AValue then Exit;
  FColor:=AValue;
  SourceMarkup.FrameColor:=Color;
end;

constructor TETMarkStyle.Create(TheUrgency: TMessageLineUrgency;
  TheColor: TColor);
begin
  FUrgency:=TheUrgency;
  FColor:=TheColor;
  FSourceMarkup:=TSynSelectedColor.Create;
  SourceMarkup.Foreground:=clNone;
  SourceMarkup.Background:=clNone;
  SourceMarkup.FrameStyle:=slsWaved;
  SourceMarkup.FrameEdges:=sfeBottom;
  SourceMarkup.FrameColor:=Color;
end;

destructor TETMarkStyle.Destroy;
begin
  FreeAndNil(FSourceMarkup);
  inherited Destroy;
end;

{ TLMsgViewLine }

destructor TLMsgViewLine.Destroy;
begin
  FreeAndNil(Mark);
  inherited Destroy;
end;

end.

