unit TestWordWrap;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, math, TestBase, SynEditViewedLineMap, SynEditMiscClasses,
  SynEditTypes, SynEditWrappedView,
  LazSynEditText, SynEditHighlighterFoldBase, LazLoggerBase,
  SynEditKeyCmds, SynEdit, SynEditPointClasses, testregistry;

type
  TIntArray = Array of integer;

  { TExpWraps }

  TExpWraps = object
    w: Array of Integer;
    len: Integer;
    function Init(const a: array of integer): TExpWraps;
    procedure SetCapacity(l: Integer);
    procedure InitFill(AFrom, ATo: integer; AIncrease: Integer = 1);
    procedure FillRange(AStartIdx, ACount, AFromVal: integer; AIncrease: Integer = 1);
    procedure Join(const a: TExpWraps; AInsertPos: Integer = -1);
    procedure Join(const a: array of integer; AInsertPos: Integer = -1);
    procedure SpliceArray(ADelFrom, ADelCount: integer);
  end;

  { TTestWordWrapBase }

  TTestWordWrapBase = class(TTestBase)
  protected
    function TheTree: TSynLineMapAVLTree; virtual; abstract;
    function  TreeNodeCount: integer;
    procedure CheckTree(AName: String); virtual;
    procedure CheckTree(AName: String; ANode: TSynEditLineMapPage; ANodeLine: TLineIdx; AMinLine, AMaxLine: TLineIdx);

  end;

  { TTestWordWrap }

  TTestWordWrap = class(TTestWordWrapBase)
  private
    FTree: TSynLineMapAVLTree;
    procedure AssertRealToWrapOffsets(const AName: String; ALine: TSynWordWrapLineMap;
      const ExpWrapOffsets: TExpWraps; AStartOffs: Integer = 0);
    procedure AssertWrapToRealOffset(const AName: String; ALine: TSynWordWrapLineMap;
      const ExpRealAndSubOffsets: TExpWraps; AStartOffs: Integer = 0);
    procedure AssertLineForWraps(const AName: String; ALine: TSynWordWrapLineMap;
      const ExpWrapForEachLine: TExpWraps; AnExpAllValid: Boolean = False);
    procedure InitLine(ALine: TSynWordWrapLineMap;
      const AWrapValues: TExpWraps);
    function OnPageNeeded(AMapTree: TSynLineMapAVLTree): TSynEditLineMapPage;
    procedure ValidateWraps(ALine: TSynWordWrapLineMap;
      const AWrapValues: TExpWraps; AStartOffs: Integer = 0; ABackward: Boolean = False);
    procedure ValidateNeededWraps(ALine: TSynWordWrapLineMap; const AWrapValues: TExpWraps);

    procedure ValidateTreeWraps(const AWrapValues: TExpWraps; AStartOffs: Integer = 0);
    procedure AssertTreeForWraps(const AName: String; const ExpWrapForEachLine: TExpWraps; AStartOffs: Integer = 0);

    function CreateTree(APageJoinSize, APageSplitSize, APageJoinDistance: Integer): TSynLineMapAVLTree;
  protected
    function TheTree: TSynLineMapAVLTree; override;

    procedure SetUp; override;
    procedure TearDown; override;
  published
    procedure TestWordWrapLineMap;
    procedure TestWordWrapLineMapInvalidate;
    procedure TestWordWrapLineMapInvalidateNoneContineous;
    procedure TestWordWrapLineMapValidate;
    procedure TestWordWrapLineMapMerge;
    procedure TestWordWrapLineMapMergeInvalidate;
    procedure TestWordWrapJoinWithSibling;

    procedure TestWordWrapTreeInsertThenDelete;
    procedure TestWordWrapTreeDeleteThenInsert;
  end;

  TPointType = (ptViewed, ptAlternateViewed, ptPhys, ptLog);

  TPointSpecs = record
    XY: array [TPointType] of TPoint;
    LogOffs: Integer;
  end;

  TCommandAndPointSpecs = record
    Exp: TPointSpecs;
    Cmd: Array of TSynEditorCommand;
    RunOnlyIf: Boolean;
  end;

  TTestWrapLineInfo = record
    TextIdx: TLineIdx;
    ViewedIdx, ViewedTopIdx, ViewedBottomIdx: TLineIdx;
    SubIdx: Integer;
    //FirstLogX: Integer;
    TextStartMatch: String;
    NoTrim: Boolean;
  end;
  TTestViewedLineRangeInfo = array of TTestWrapLineInfo;

  TTripleBool = (tTrue, tFalse, tKeep);

function l(ATxtIdx: TLineIdx; ASubIdx: Integer; AText: String; ANoTrim: Boolean = False): TTestWrapLineInfo;
function ViewedExp(AFirstViewedIdx: TLineIdx; ALines:
  array of TTestWrapLineInfo; ANoTrim: TTripleBool = tKeep): TTestViewedLineRangeInfo;

type

  { TTestWordWrapPluginBase }

  TTestWordWrapPluginBase = class(TTestWordWrapBase)
  private
    procedure ClearCaret;
    function GetTreeNodeHolder(AIndex: Integer): TSynEditLineMapPageHolder;
    procedure SetCaret(SourcePt: TPointType; APos: TPoint);
    procedure TestCaret(AName: String; SourcePt, ExpPt: TPointType; AnExp: TPoint;
      AnExpOffs: Integer = -1);
  protected
    FWordWrap: TLazSynEditLineWrapPlugin;
    class procedure AssertEquals(const AMessage: string; Expected, Actual: TPoint); overload;
    procedure AddLines(AFirstLineIdx, ACount, ALen: Integer; AnID: String; SkipBeginUpdate: Boolean = False; AReplaceExisting: Boolean = False);
    procedure InternalCheckLine(AName: String; dsp: TLazSynDisplayView; ALine: TLineIdx; AExpTextStart: String; NoTrim: Boolean = False);
    procedure CheckLine(AName: String; ALine: TLineIdx; AExpTextStart: String; NoTrim: Boolean = False);
    procedure CheckLines(AName: String; AStartLine: TLineIdx; AExpTextStart: array of String; NoTrim: Boolean = False);

    procedure CheckLine(AName: String; AExpLine: TTestWrapLineInfo);
    procedure CheckLines(AName: String; AExpLines: TTestViewedLineRangeInfo);

    procedure CheckXyMap(AName: String; APhysTExtXY, AViewedXY: TPoint; OnlyViewToText: Boolean = False);
    procedure CheckXyMap(AName: String; APhysTExtX, APhysTExtY, AViewedX, AViewedY: integer; OnlyViewToText: Boolean = False);

    procedure CheckXyMap(AName: String; APoints: TPointSpecs);
    procedure CheckXyMap(AName: String; APoints: TPointSpecs;
      ATestCommands: array of TCommandAndPointSpecs);

    procedure CheckLineIndexMapping(AName: String; ATextIdx, AViewTopIdx, AViewBottomIdx: TLineIdx);

    function TheTree: TSynLineMapAVLTree; override;
    property TreeNodeHolder[AIndex: Integer]: TSynEditLineMapPageHolder read GetTreeNodeHolder;

    procedure ReCreateEdit(ADispWidth: Integer);
    procedure SetUp; override;
    procedure TearDown; override;
  end;

  TTestWordWrapPlugin = class(TTestWordWrapPluginBase)
  published
    procedure TestEditorWrap;
    procedure TestWrapSplitJoin;
    procedure TestEditorEdit;
  end;

implementation

function p(VX, VY,  AVX, AVY,  PX, PY,  LX, LY: Integer; Offs: Integer = -1): TPointSpecs; overload;
begin
  with Result do begin
    XY[ptViewed].X          := VX;
    XY[ptViewed].Y          := VY;
    XY[ptAlternateViewed].X := AVX;
    XY[ptAlternateViewed].Y := AVY;
    XY[ptPhys].X            := PX;
    XY[ptPhys].Y            := PY;
    XY[ptLog].X             := LX;
    XY[ptLog].Y             := LY;
    LogOffs                 := Offs;
  end;
end;

function p(VX, VY,  PX, PY,  LX, LY: Integer; Offs: Integer = -1): TPointSpecs; overload;
begin
  Result := p(VX, VY, -1, -1, PX, PY, LX, LY, Offs);
end;

function c(Cmd: Array of TSynEditorCommand; VX, VY,  AVX, AVY,  PX, PY,  LX, LY: Integer; Offs: Integer = -1; RunOnlyIf: Boolean = True): TCommandAndPointSpecs; overload;
begin
  Result.Exp := p(VX, VY, AVX, AVY, PX, PY, LX, LY, Offs);
  SetLength(Result.Cmd, Length(Cmd));
  move(Cmd[0], Result.Cmd[0], SizeOf(cmd[0]) * Length(Cmd));
  Result.RunOnlyIf := RunOnlyIf;
end;

function c(Cmd: Array of TSynEditorCommand; VX, VY,  PX, PY,  LX, LY: Integer; Offs: Integer = -1; RunOnlyIf: Boolean = True): TCommandAndPointSpecs; overload;
begin
  Result := c(Cmd, VX, VY, -1, -1, PX, PY, LX, LY, Offs, RunOnlyIf);
end;

function c(Cmd: TSynEditorCommand; VX, VY,  PX, PY,  LX, LY: Integer; Offs: Integer = -1; RunOnlyIf: Boolean = True): TCommandAndPointSpecs; overload;
begin
  Result := c([Cmd], VX, VY, -1, -1, PX, PY, LX, LY, Offs, RunOnlyIf);
end;

function FillArray(AFrom, ATo: integer; AIncrease: Integer = 1): TIntArray;
var
  i: Integer;
begin
  SetLength(Result, ATo - AFrom + 1);
  for i := 0 to high(Result) do
    Result[i] := AFrom + i * AIncrease;
end;

function l(ATxtIdx: TLineIdx; ASubIdx: Integer; AText: String; ANoTrim: Boolean
  ): TTestWrapLineInfo;
begin
  Result.TextIdx := ATxtIdx;
  Result.SubIdx  := ASubIdx;
  Result.TextStartMatch := AText;
  Result.NoTrim  := ANoTrim;
end;

function ViewedExp(AFirstViewedIdx: TLineIdx;
  ALines: array of TTestWrapLineInfo; ANoTrim: TTripleBool
  ): TTestViewedLineRangeInfo;
var
  i, j: Integer;
begin
  SetLength(Result, Length(ALines));
  j := 0;
  for i := 0 to Length(ALines) - 1 do begin
    if (i > 0) and (ALines[i].SubIdx = 0) then begin
      while j < i do begin
        Result[j].ViewedBottomIdx := AFirstViewedIdx - 1;
        inc(j);
      end;
      j := i;
    end;
    Result[i] := ALines[i];
    Result[i].ViewedIdx    := AFirstViewedIdx;
    Result[i].ViewedTopIdx := AFirstViewedIdx - Result[i].SubIdx;
    case ANoTrim of
      tFalse: Result[i].NoTrim := False;
      tTrue:  Result[i].NoTrim := True;
    end;
    inc(AFirstViewedIdx);
  end;
  while j < Length(ALines) do begin
    Result[j].ViewedBottomIdx := AFirstViewedIdx - 1;
    inc(j);
  end;
end;

{ TTestWordWrapBase }

function TTestWordWrapBase.TreeNodeCount: integer;
var
  n: TSynEditLineMapPageHolder;
begin
  Result := 0;
  n := TheTree.FirstPage;
  while n.HasPage do begin
    inc(Result);
    n := n.Next;
  end;
end;

procedure TTestWordWrapBase.CheckTree(AName: String);
var
  n: TSynEditLineMapPageHolder;
begin
  if TheTree = nil then
    AssertTrue(AName, False);
  n := TheTree.FirstPage;
  if n.HasPage then
    CheckTree(AName, n.Page, n.StartLine, 0, MaxInt);
end;

procedure TTestWordWrapBase.CheckTree(AName: String;
  ANode: TSynEditLineMapPage; ANodeLine: TLineIdx; AMinLine, AMaxLine: TLineIdx
  );
var
  n: TSynEditLineMapPage;
  dummy, i, EndLine: Integer;
  nl: TLineIdx;
begin
  nl := ANodeLine;
  n := ANode.Precessor(nl, dummy);
  while n <> nil do begin
    ANode := n;
    ANodeLine := nl;
    n := ANode.Precessor(nl, dummy);
  end;

  i := 0;
  while ANode <> nil do begin
    AssertTrue(Format('%s(%d): MinLine', [AName, i]), ANodeLine >= AMinLine);
    EndLine := ANodeLine + Max(0, ANode.RealEndLine);
    AssertTrue(Format('%s(%d): EndLine', [AName, i]), EndLine <= AMaxLine);

    AMinLine := EndLine + 1;
    ANode := ANode.Successor(ANodeLine, dummy);
    inc(i);
  end;
end;

{ TExpWraps }

function TExpWraps.Init(const a: array of integer): TExpWraps;
begin
  len := Length(a);
  if len > 0 then begin
    SetCapacity(len);
    move(a[0], w[0], SizeOf(w[0]) * len);
  end;
  Result := self;
end;

procedure TExpWraps.SetCapacity(l: Integer);
begin
  if Length(w) < l then
    SetLength(w, l*2);
end;

procedure TExpWraps.InitFill(AFrom, ATo: integer; AIncrease: Integer);
var
  p: PLongInt;
  i: Integer;
begin
  len := ATo - AFrom + 1;
  SetCapacity(len);
  p := @w[0];
  for i := 0 to len - 1 do begin
    p^ := AFrom;
    inc(p);
    inc(AFrom, AIncrease);
  end;
end;

procedure TExpWraps.FillRange(AStartIdx, ACount, AFromVal: integer;
  AIncrease: Integer);
var
  p: PLongInt;
  i: Integer;
begin
  if len < AStartIdx + ACount then
    len := AStartIdx + ACount;
  SetCapacity(len);

  p := @w[AStartIdx];
  for i := 0 to ACount - 1 do begin
    p^ := AFromVal;
    inc(p);
    inc(AFromVal, AIncrease);
  end;

end;

procedure TExpWraps.Join(const a: TExpWraps; AInsertPos: Integer);
var
  i, old: Integer;
begin
  if AInsertPos < 0 then
    AInsertPos := Len;

  i := (Len-AInsertPos);
  old := len;
  len := len + a.len;
  if i < 0 then
    len := len - i;
  SetCapacity(len);

  if i > 0 then begin
    move(w[AInsertPos], w[AInsertPos+a.len], sizeof(w[0]) * i);
  end
  else
  if i < 0 then begin
    FillDWord(w[old], -i, 1);
  end;
  move(a.w[0], w[AInsertPos], sizeof(w[0]) * a.len);
end;

procedure TExpWraps.Join(const a: array of integer; AInsertPos: Integer);
var
  i, la, old: Integer;
begin
  if AInsertPos < 0 then
    AInsertPos := Len;

  i := (Len-AInsertPos);
  la := Length(a);
  old := len;
  len := len + la;
  if i < 0 then
    len := len - i;
  SetCapacity(len);

  if i > 0 then begin
    move(w[AInsertPos], w[AInsertPos+la], sizeof(w[0]) * i);
  end
  else
  if i < 0 then begin
    FillDWord(w[old], -i, 1);
  end;
  move(a[0], w[AInsertPos], sizeof(w[0]) * la);
end;

procedure TExpWraps.SpliceArray(ADelFrom, ADelCount: integer);
var
  i: Integer;
begin
  len := len - ADelCount;

  i := Length(w) - ADelFrom - ADelCount;
  if i > 0 then
    move(w[ADelFrom+ADelCount], w[ADelFrom], sizeof(w[0]) * (i));
end;

{ TTestWordWrap }

procedure TTestWordWrap.AssertRealToWrapOffsets(const AName: String;
  ALine: TSynWordWrapLineMap; const ExpWrapOffsets: TExpWraps;
  AStartOffs: Integer);
var
  i: Integer;
begin
  for i := 0 to ExpWrapOffsets.len - 1 do
    AssertEquals(format('%s: RealToWrap Idx %d StartOffs: %d ', [AName, i, AStartOffs]),
      ExpWrapOffsets.w[i], ALine.WrappedOffsetFor[AStartOffs + i]);
end;

procedure TTestWordWrap.AssertWrapToRealOffset(const AName: String;
  ALine: TSynWordWrapLineMap; const ExpRealAndSubOffsets: TExpWraps;
  AStartOffs: Integer);
var
  i, sub, r: Integer;
begin
  for i := 0 to ExpRealAndSubOffsets.len div 2 - 1 do begin
    r := ALine.GetOffsetForWrap(AStartOffs + i, sub);
    AssertEquals(format('%s: WrapToReal Idx %d StartOffs: %d ', [AName, i, AStartOffs]),
      ExpRealAndSubOffsets.w[i*2], r);
    AssertEquals(format('%s: WrapToReal(SUB) Idx %d StartOffs: %d ', [AName, i, AStartOffs]),
      ExpRealAndSubOffsets.w[i*2+1], sub);
  end;
end;

procedure TTestWordWrap.AssertLineForWraps(const AName: String;
  ALine: TSynWordWrapLineMap; const ExpWrapForEachLine: TExpWraps;
  AnExpAllValid: Boolean);
var
  i, j, ExpWrap, TestWrapToReal, GotReal, sub: Integer;
begin
  if AnExpAllValid then
    AssertTrue(AName + ' - all lines valid', ALine.FirstInvalidLine < 0);
  i := 0;
  while (i < ExpWrapForEachLine.len) and (ExpWrapForEachLine.w[i] = 1) do
    inc(i);
  if i = ExpWrapForEachLine.len then
    i := 0;
  AssertEquals(Format('%s: Offset', [AName]), i, ALine.Offset);

  j := ExpWrapForEachLine.len - 1;
  while (j >= 0) and (ExpWrapForEachLine.w[j] = 1) do
    dec(j);
  AssertEquals(Format('%s: RealCount', [AName]), j + 1 - i, ALine.RealCount);

  ExpWrap := 0;
  TestWrapToReal := 0;
  for i := 0 to ExpWrapForEachLine.len - 1 do begin
    AssertEquals(Format('%s: RealToWrap Idx %d', [AName, i]), ExpWrap, ALine.WrappedOffsetFor[i]);
    ExpWrap := ExpWrap + ExpWrapForEachLine.w[i];

    for j := 0 to ExpWrapForEachLine.w[i] - 1 do begin
      GotReal := ALine.GetOffsetForWrap(TestWrapToReal, sub);
      AssertEquals(Format('%s: WrapToReal Idx %d', [AName, TestWrapToReal]), i, GotReal);
      AssertEquals(Format('%s: WrapToReal Idx %d SUB', [AName, TestWrapToReal]), j, sub);
      inc(TestWrapToReal);
    end;
  end;

  CheckTree(AName+'TreeCheck');
end;

procedure TTestWordWrap.InitLine(ALine: TSynWordWrapLineMap;
  const AWrapValues: TExpWraps);
begin
  ALine.DeleteLinesAtOffset(0, max(ALine.RealCount + ALine.Offset, ALine.LastInvalidLine+1));
  if AWrapValues.len > 0 then begin
    ALine.InsertLinesAtOffset(0, AWrapValues.len);
    ValidateWraps(ALine, AWrapValues);
  end;
  AssertEquals('all valid', -1, ALine.FirstInvalidLine);
end;

function TTestWordWrap.OnPageNeeded(AMapTree: TSynLineMapAVLTree
  ): TSynEditLineMapPage;
begin
  Result := TSynWordWrapIndexPage.Create(AMapTree);
  //TSynWordWrapIndexPage(Result).FSynEditWrappedPlugin := Self;
end;

procedure TTestWordWrap.ValidateWraps(ALine: TSynWordWrapLineMap;
  const AWrapValues: TExpWraps; AStartOffs: Integer; ABackward: Boolean);
var
  i: Integer;
begin
  if ABackward then begin
    for i := AWrapValues.len - 1 downto 0 do
      ALine.ValidateLine(AStartOffs + i, AWrapValues.w[i]);
  end
  else begin
    for i := 0 to AWrapValues.len - 1 do
      ALine.ValidateLine(AStartOffs + i, AWrapValues.w[i]);
  end;
  ALine.EndValidate;
end;

procedure TTestWordWrap.ValidateNeededWraps(ALine: TSynWordWrapLineMap;
  const AWrapValues: TExpWraps);
var
  i: Integer;
begin
  i := ALine.FirstInvalidLine;
  while i >= 0 do begin
    ALine.ValidateLine(i, AWrapValues.w[i]);
    i := ALine.FirstInvalidLine;
  end;
  ALine.EndValidate;
end;

procedure TTestWordWrap.ValidateTreeWraps(const AWrapValues: TExpWraps;
  AStartOffs: Integer);
var
  i: Integer;
  LowLine, HighLine: TLineIdx;
begin
  while FTree.NextBlockForValidation(LowLine, HighLine) do begin
    for i := LowLine to HighLine do begin
      AssertTrue(i-AStartOffs < AWrapValues.len);
      FTree.ValidateLine(i, AWrapValues.w[i-AStartOffs]);
    end;
  end;
  FTree.EndValidate;
end;

procedure TTestWordWrap.AssertTreeForWraps(const AName: String;
  const ExpWrapForEachLine: TExpWraps; AStartOffs: Integer);
var
  i, w: Integer;
  sub: TLineIdx;
begin
  w := AStartOffs;
  for i := 0 to (ExpWrapForEachLine.len - 1) do begin
    AssertEquals(Format('%s // l=%d getWrap', [AName, i]),
      w,
      FTree.GetWrapLineForForText(AStartOffs + i)
    );
    w := w + ExpWrapForEachLine.w[i];
    AssertEquals(Format('%s // l=%d getLine', [AName, i]),
      i,
      FTree.GetLineForForWrap(w-1, sub)
    );
    AssertEquals(Format('%s // l=%d sub', [AName, i]),
      ExpWrapForEachLine.w[i]-1,
      sub
    );
  end;

  CheckTree(AName+'TreeCheck');
end;

function TTestWordWrap.CreateTree(APageJoinSize, APageSplitSize,
  APageJoinDistance: Integer): TSynLineMapAVLTree;
begin
  Result := TSynLineMapAVLTree.Create(APageJoinSize, APageSplitSize, APageJoinDistance);
  Result.PageCreatorProc := @OnPageNeeded;
end;

function TTestWordWrap.TheTree: TSynLineMapAVLTree;
begin
  Result := FTree;
end;

procedure TTestWordWrap.SetUp;
begin
  FTree := CreateTree(15, 60, 20);
  inherited SetUp;
end;

procedure TTestWordWrap.TearDown;
begin
  inherited TearDown;
  FTree.Free;
end;

procedure TTestWordWrap.TestWordWrapLineMap;
var
  ALine: TSynWordWrapLineMap;
  ANode: TSynEditLineMapPage;
  i: Integer;
  ATestName: String;
  w: TExpWraps;
begin
  ANode := FTree.FindPageForLine(0, afmCreate).Page;
  ALine := TSynWordWrapIndexPage(ANode).SynWordWrapLineMapStore;
  ALine.InsertLinesAtOffset(0, 5);
  ALine.InvalidateLines(2,3);
  ValidateWraps(ALine, w.init([1, 1, 3, 3, 1]));
  AssertLineForWraps('', ALine, w.init([1, 1, 3, 3, 1,   1,1]));
  //AssertRealToWrapOffsets('', ALine, [0, 1, 2, 5, 8, 9, 10]);
  //AssertWrapToRealOffset('', ALine, [0,0,  1,0,  2,0, 2,1, 2,2,  3,0, 3,1, 3,2,  4,0,  5,0]);
  AssertEquals('all valid', -1, ALine.FirstInvalidLine);

  for i := 1 to 2 do begin

    // insert into offset
    ATestName := 'Insert at start of "Offset"';
    ALine.InsertLinesAtOffset(0, 2);
    ValidateWraps(ALine, w.init([2, 2]), 0, i mod 1 = 1);
    AssertLineForWraps(ATestName, ALine, w.init([2, 2,   1, 1, 3, 3, 1,   1,1]));
    AssertEquals('all valid', -1, ALine.FirstInvalidLine);

    ALine.DeleteLinesAtOffset(0, 2);
    AssertLineForWraps(ATestName, ALine, w.init([1, 1, 3, 3, 1,   1,1]));
    AssertEquals('all valid', -1, ALine.FirstInvalidLine);


    ATestName := 'Insert at middle of "Offset"';
    ALine.InsertLinesAtOffset(1, 2);
    ValidateWraps(ALine, w.init([2, 2]), 1, i mod 1 = 1);
    AssertLineForWraps(ATestName, ALine, w.init([1,   2, 2,   1, 3, 3, 1,   1,1]));
    AssertEquals('all valid', -1, ALine.FirstInvalidLine);

    ALine.DeleteLinesAtOffset(1, 2);
    AssertLineForWraps(ATestName, ALine, w.init([1, 1, 3, 3, 1,   1,1]));
    AssertEquals('all valid', -1, ALine.FirstInvalidLine);


    ATestName := 'Insert at end of "Offset"';
    ALine.InsertLinesAtOffset(2, 2);
    ValidateWraps(ALine, w.init([2, 2]), 2, i mod 1 = 1);
    AssertLineForWraps(ATestName, ALine, w.init([1, 1,   2, 2,   3, 3, 1,   1,1]));
    AssertEquals('all valid', -1, ALine.FirstInvalidLine);

    ALine.DeleteLinesAtOffset(2, 2);
    AssertLineForWraps(ATestName, ALine, w.init([1, 1, 3, 3, 1,   1,1]));
    AssertEquals('all valid', -1, ALine.FirstInvalidLine);



    ATestName := 'Insert at start of "Offset" - single lines';
    ALine.InsertLinesAtOffset(0, 2);
    ValidateWraps(ALine, w.init([1, 1]), 0, i mod 1 = 1);
    AssertLineForWraps(ATestName, ALine, w.init([1, 1,   1, 1, 3, 3, 1,   1,1]));
    AssertEquals('all valid', -1, ALine.FirstInvalidLine);

    ALine.DeleteLinesAtOffset(0, 2);
    AssertLineForWraps(ATestName, ALine, w.init([1, 1, 3, 3, 1,   1,1]));
    AssertEquals('all valid', -1, ALine.FirstInvalidLine);


    ATestName := 'Insert at middle of "Offset" - single lines';
    ALine.InsertLinesAtOffset(1, 2);
    ValidateWraps(ALine, w.init([1, 1]), 1, i mod 1 = 1);
    AssertLineForWraps(ATestName, ALine, w.init([1,   1, 1,   1, 3, 3, 1,   1,1]));
    AssertEquals('all valid', -1, ALine.FirstInvalidLine);

    ALine.DeleteLinesAtOffset(1, 2);
    AssertLineForWraps(ATestName, ALine, w.init([1, 1, 3, 3, 1,   1,1]));
    AssertEquals('all valid', -1, ALine.FirstInvalidLine);


    ATestName := 'Insert at end of "Offset" - single lines';
    ALine.InsertLinesAtOffset(2, 2);
    ValidateWraps(ALine, w.init([1, 1]), 2, i mod 1 = 1);
    AssertLineForWraps(ATestName, ALine, w.init([1, 1,   1, 1,   3, 3, 1,   1,1]));
    AssertEquals('all valid', -1, ALine.FirstInvalidLine);

    ALine.DeleteLinesAtOffset(2, 2);
    AssertLineForWraps(ATestName, ALine, w.init([1, 1, 3, 3, 1,   1,1]));
    AssertEquals('all valid', -1, ALine.FirstInvalidLine);



    ATestName := 'Insert at start of "Offset" - single/wrap lines';
    ALine.InsertLinesAtOffset(0, 2);
    ValidateWraps(ALine, w.init([1, 2]), 0, i mod 1 = 1);
    AssertLineForWraps(ATestName, ALine, w.init([1, 2,   1, 1, 3, 3, 1,   1,1]));
    AssertEquals('all valid', -1, ALine.FirstInvalidLine);

    ALine.DeleteLinesAtOffset(1, 1);
    AssertLineForWraps(ATestName, ALine, w.init([1,   1, 1, 3, 3, 1,   1,1]));
    AssertEquals('all valid', -1, ALine.FirstInvalidLine);
    ALine.DeleteLinesAtOffset(0, 1);
    AssertLineForWraps(ATestName, ALine, w.init([1, 1, 3, 3, 1,   1,1]));
    AssertEquals('all valid', -1, ALine.FirstInvalidLine);


    ATestName := 'Delete mixed offset/data';
    ALine.DeleteLinesAtOffset(1, 2);
    AssertLineForWraps(ATestName, ALine, w.init([1,    3, 1,   1,1]));
    AssertEquals('all valid', -1, ALine.FirstInvalidLine);

    ALine.InsertLinesAtOffset(1, 2);
    ValidateWraps(ALine, w.init([1, 3]), 1, i mod 1 = 1);
    AssertLineForWraps(ATestName, ALine, w.init([1, 1, 3, 3, 1,   1,1]));
    AssertEquals('all valid', -1, ALine.FirstInvalidLine);


    // insert into data
    ATestName := 'Insert at middle of Data';
    ALine.InsertLinesAtOffset(3, 2);
    ValidateWraps(ALine, w.init([2, 2]), 3, i mod 1 = 1);
    AssertLineForWraps(ATestName, ALine, w.init([1, 1, 3,   2, 2,   3, 1,   1,1]));
    AssertEquals('all valid', -1, ALine.FirstInvalidLine);

    ALine.DeleteLinesAtOffset(3, 2);
    AssertLineForWraps(ATestName, ALine, w.init([1, 1, 3, 3, 1,   1,1]));
    AssertEquals('all valid', -1, ALine.FirstInvalidLine);



    ATestName := 'Insert at middle of Data';
    ALine.InsertLinesAtOffset(3, 2);
    ValidateWraps(ALine, w.init([2, 2]), 3, i mod 1 = 1);
    AssertLineForWraps(ATestName, ALine, w.init([1, 1, 3,   2, 2,   3, 1,   1,1]));
    AssertEquals('all valid', -1, ALine.FirstInvalidLine);

    ALine.DeleteLinesAtOffset(3, 2);
    AssertLineForWraps(ATestName, ALine, w.init([1, 1, 3, 3, 1,   1,1]));
    AssertEquals('all valid', -1, ALine.FirstInvalidLine);



    // insert after data
    ATestName := 'Insert at end of Data';
    ALine.InsertLinesAtOffset(5, 1);
    ValidateWraps(ALine, w.init([4]), 5, i mod 1 = 1);
    AssertLineForWraps(ATestName, ALine, w.init([1, 1, 3, 3, 1,   4,   1,1]));
    AssertEquals('all valid', -1, ALine.FirstInvalidLine);

    ALine.DeleteLinesAtOffset(5, 1);
    AssertLineForWraps(ATestName, ALine, w.init([1, 1, 3, 3, 1,   1,1]));
    AssertEquals('all valid', -1, ALine.FirstInvalidLine);


    ATestName := 'Insert at end of Data - single line';
    ALine.InsertLinesAtOffset(5, 1);
    ValidateWraps(ALine, w.init([1]), 5, i mod 1 = 1);
    AssertLineForWraps(ATestName, ALine, w.init([1, 1, 3, 3, 1,   1,   1,1]));
    AssertEquals('all valid', -1, ALine.FirstInvalidLine);

    ALine.DeleteLinesAtOffset(5, 1);
    AssertLineForWraps(ATestName, ALine, w.init([1, 1, 3, 3, 1,   1,1]));
    AssertEquals('all valid', -1, ALine.FirstInvalidLine);


    ATestName := 'Insert behind end of Data';
    ALine.InsertLinesAtOffset(6, 1);
    ValidateWraps(ALine, w.init([4]), 6, i mod 1 = 1);
    AssertLineForWraps(ATestName, ALine, w.init([1, 1, 3, 3, 1,   1, 4,   1,1]));
    AssertEquals('all valid', -1, ALine.FirstInvalidLine);

    ALine.DeleteLinesAtOffset(6, 1);
    AssertLineForWraps(ATestName, ALine, w.init([1, 1, 3, 3, 1,   1,1]));
    AssertEquals('all valid', -1, ALine.FirstInvalidLine);


    ATestName := 'Insert behind end of Data - single line';
    ALine.InsertLinesAtOffset(6, 1);
    ValidateWraps(ALine, w.init([1]), 6, i mod 1 = 1);
    AssertLineForWraps(ATestName, ALine, w.init([1, 1, 3, 3, 1,   1, 1,   1,1]));
    AssertEquals('all valid', -1, ALine.FirstInvalidLine);

    ALine.DeleteLinesAtOffset(6, 1);
    AssertLineForWraps(ATestName, ALine, w.init([1, 1, 3, 3, 1,   1,1]));
    AssertEquals('all valid', -1, ALine.FirstInvalidLine);


    ATestName := 'Delete mixed data/after';
    ALine.DeleteLinesAtOffset(3, 2);
    AssertLineForWraps(ATestName, ALine, w.init([1, 1, 3,    1,1]));
    AssertEquals('all valid', -1, ALine.FirstInvalidLine);

    ALine.InsertLinesAtOffset(3, 2);
    ValidateWraps(ALine, w.init([3, 1]), 3, i mod 1 = 1);
    AssertLineForWraps(ATestName, ALine, w.init([1, 1, 3, 3, 1,   1,1]));
    AssertEquals('all valid', -1, ALine.FirstInvalidLine);

  end;

  ALine.InvalidateLines(0, 4);
  ValidateWraps(ALine, w.init([1,1,1,1,1]), 0, False);
  AssertLineForWraps('', ALine, w.init([1, 1, 1, 1, 1,   1,1]));
  AssertEquals('all valid', -1, ALine.FirstInvalidLine);

  ALine.InsertLinesAtOffset(0, 5);
  ValidateWraps(ALine, w.init([1, 1, 3, 3, 1]));
  AssertLineForWraps('', ALine, w.init([1, 1, 3, 3, 1,   1,1]));
  AssertEquals('all valid', -1, ALine.FirstInvalidLine);

  ALine.InvalidateLines(0, 4);
  ValidateWraps(ALine, w.init([1,1,1,1,1]), 0, True);
  AssertLineForWraps('', ALine, w.init([1, 1, 1, 1, 1,   1,1]));
  AssertEquals('all valid', -1, ALine.FirstInvalidLine);

end;

procedure TTestWordWrap.TestWordWrapLineMapInvalidate;
var
  ANode1: TSynWordWrapIndexPage;
  ALine1: TSynWordWrapLineMap;
  //ATestName: String;
  w: TExpWraps;
begin
  // invalidate and insert/remove lines
  ANode1 := TSynWordWrapIndexPage(FTree.FindPageForLine(0, afmCreate).Page);
  ALine1 := ANode1.SynWordWrapLineMapStore;

  InitLine(ALine1, w.init([1]));
  ALine1.InvalidateLines(3,6);
  AssertEquals('invalid', 3, ALine1.FirstInvalidLine);
  AssertEquals('invalid', 6, ALine1.LastInvalidLine);

  ALine1.DeleteLinesAtOffset(6,2);
  AssertEquals('invalid', 3, ALine1.FirstInvalidLine);
  AssertEquals('invalid', 5, ALine1.LastInvalidLine);

  ALine1.InsertLinesAtOffset(2,1);
  AssertEquals('invalid', 2, ALine1.FirstInvalidLine);
  ValidateWraps(ALine1, w.init([1]), 2);

  AssertEquals('invalid', 4, ALine1.FirstInvalidLine);
  AssertEquals('invalid', 6, ALine1.LastInvalidLine);

  ALine1.InsertLinesAtOffset(5,1);
  AssertEquals('invalid', 4, ALine1.FirstInvalidLine);
  AssertEquals('invalid', 7, ALine1.LastInvalidLine);

  ALine1.DeleteLinesAtOffset(4,1);
  AssertEquals('invalid', 4, ALine1.FirstInvalidLine);
  AssertEquals('invalid', 6, ALine1.LastInvalidLine);

end;

procedure TTestWordWrap.TestWordWrapLineMapInvalidateNoneContineous;
var
  ANode1: TSynWordWrapIndexPage;
  ALine1: TSynWordWrapLineMap;
  //ATestName: String;
  w: TExpWraps;
begin
  // invalidate and insert/remove lines
  ANode1 := TSynWordWrapIndexPage(FTree.FindPageForLine(0, afmCreate).Page);
  ALine1 := ANode1.SynWordWrapLineMapStore;

  InitLine(ALine1, w.init([1]));
  ALine1.InvalidateLines(30,31);
  ALine1.InvalidateLines(32,35);
  ALine1.InvalidateLines(40,41);
  ALine1.InvalidateLines(10,11);
  ALine1.InvalidateLines(20,21);
  ALine1.InvalidateLines(22,23);

  AssertEquals('invalid first from', 10, ALine1.FirstInvalidLine);
  AssertEquals('invalid first to',   11, ALine1.FirstInvalidEndLine);
  AssertEquals('invalid last',       41, ALine1.LastInvalidLine);

  ALine1.ValidateLine(10, 1);
  AssertEquals('invalid first from', 11, ALine1.FirstInvalidLine);
  AssertEquals('invalid first to',   11, ALine1.FirstInvalidEndLine);
  AssertEquals('invalid last',       41, ALine1.LastInvalidLine);

  ALine1.ValidateLine(11, 1);
  AssertEquals('invalid first from', 20, ALine1.FirstInvalidLine);
  AssertEquals('invalid first to',   23, ALine1.FirstInvalidEndLine);
  AssertEquals('invalid last',       41, ALine1.LastInvalidLine);

  ALine1.ValidateLine(20, 1);
  AssertEquals('invalid first from', 21, ALine1.FirstInvalidLine);
  AssertEquals('invalid first to',   23, ALine1.FirstInvalidEndLine);
  AssertEquals('invalid last',       41, ALine1.LastInvalidLine);

  ALine1.ValidateLine(21, 1);
  ALine1.ValidateLine(22, 1);
  ALine1.ValidateLine(23, 1);
  AssertEquals('invalid first from', 30, ALine1.FirstInvalidLine);
  AssertEquals('invalid first to',   35, ALine1.FirstInvalidEndLine);
  AssertEquals('invalid last',       41, ALine1.LastInvalidLine);

  ALine1.ValidateLine(30, 1);
  ALine1.ValidateLine(31, 1);
  ALine1.ValidateLine(32, 1);
  ALine1.ValidateLine(33, 1);
  ALine1.ValidateLine(34, 1);
  ALine1.ValidateLine(35, 1);
  AssertEquals('invalid first from', 40, ALine1.FirstInvalidLine);
  AssertEquals('invalid first to',   41, ALine1.FirstInvalidEndLine);
  AssertEquals('invalid last',       41, ALine1.LastInvalidLine);

  ALine1.ValidateLine(40, 1);
  AssertEquals('invalid first from', 41, ALine1.FirstInvalidLine);
  AssertEquals('invalid first to',   41, ALine1.FirstInvalidEndLine);
  AssertEquals('invalid last',       41, ALine1.LastInvalidLine);

  ALine1.ValidateLine(41, 1);
  AssertEquals('invalid first from', -1, ALine1.FirstInvalidLine);
  AssertEquals('invalid first to',   -1, ALine1.FirstInvalidEndLine);
  AssertEquals('invalid last',       -1, ALine1.LastInvalidLine);

end;

procedure TTestWordWrap.TestWordWrapLineMapValidate;
var
  ANode1: TSynWordWrapIndexPage;
  ALine1: TSynWordWrapLineMap;
  ATestName: String;
  w: TExpWraps;
  i: Integer;
begin
  // invalidate/ re-validate => increase/decrease offset/tail by switching between wrap and one-line lines
  ANode1 := TSynWordWrapIndexPage(FTree.FindPageForLine(0, afmCreate).Page);
  ALine1 := ANode1.SynWordWrapLineMapStore;

  ATestName := 'fill one-lines at start - increasing';
  InitLine(ALine1, w.init(FillArray(10, 19)));
  w.Join([1,1]);
  for i := 0 to 3 do begin
    ALine1.InvalidateLines(0, 3);
    w.w[i] := 1;
    ValidateNeededWraps(ALine1, w);
    AssertLineForWraps(Format('%s %d', [ATestName, i]), ALine1, w, True);
  end;

  ATestName := 'fill one-lines at start - decreasing';
  InitLine(ALine1, w.init(FillArray(10, 19)));
  w.Join([1,1]);
  for i := 3 downto 0 do begin
    ALine1.InvalidateLines(0, 3);
    w.w[i] := 1;
    ValidateNeededWraps(ALine1, w);
    AssertLineForWraps(Format('%s %d', [ATestName, i]), ALine1, w, True);
  end;



  ATestName := 'fill one-lines at end - decreasing';
  InitLine(ALine1, w.init(FillArray(10, 19)));
  w.Join([1,1]);
  for i := 9 downto 7 do begin
    ALine1.InvalidateLines(7, 9);
    w.w[i] := 1;
    ValidateNeededWraps(ALine1, w);
    AssertLineForWraps(Format('%s %d', [ATestName, i]), ALine1, w, True);
  end;

  ATestName := 'fill one-lines at end - increasing';
  InitLine(ALine1, w.init(FillArray(10, 19)));
  w.Join([1,1]);
  for i := 7 to 9 do begin
    ALine1.InvalidateLines(7, 9);
    w.w[i] := 1;
    ValidateNeededWraps(ALine1, w);
    AssertLineForWraps(Format('%s %d', [ATestName, i]), ALine1, w, True);
  end;


  ATestName := 'fill one-lines - all, incr';
  InitLine(ALine1, w.init(FillArray(10, 19)));
  w.Join([1,1]);
  for i := 0 to 9 do begin
    ALine1.InvalidateLines(0, 9);
    w.w[i] := 1;
    ValidateNeededWraps(ALine1, w);
    AssertLineForWraps(Format('%s %d', [ATestName, i]), ALine1, w, True);
  end;

  ATestName := 'fill one-lines - all, decr';
  InitLine(ALine1, w.init(FillArray(10, 19)));
  w.Join([1,1]);
  for i := 9 downto 0 do begin
    ALine1.InvalidateLines(0, 9);
    w.w[i] := 1;
    ValidateNeededWraps(ALine1, w);
    AssertLineForWraps(Format('%s %d', [ATestName, i]), ALine1, w, True);
  end;


  ATestName := 'fill one-lines - all, incr then decr';
  InitLine(ALine1, w.init(FillArray(10, 19)));
  w.Join([1,1]);
  for i := 0 to 4 do begin
    ALine1.InvalidateLines(0, 9);
    w.w[i] := 1;
    ValidateNeededWraps(ALine1, w);
    AssertLineForWraps(Format('%s %d', [ATestName, i]), ALine1, w, True);
  end;
  for i := 9 downto 5 do begin
    ALine1.InvalidateLines(0, 9);
    w.w[i] := 1;
    ValidateNeededWraps(ALine1, w);
    AssertLineForWraps(Format('%s %d', [ATestName, i]), ALine1, w, True);
  end;

  ATestName := 'fill one-lines - all, decr then incr';
  InitLine(ALine1, w.init(FillArray(10, 19)));
  w.Join([1,1]);
  for i := 9 downto 5 do begin
    ALine1.InvalidateLines(0, 9);
    w.w[i] := 1;
    ValidateNeededWraps(ALine1, w);
    AssertLineForWraps(Format('%s %d', [ATestName, i]), ALine1, w, True);
  end;
  for i := 0 to 4 do begin
    ALine1.InvalidateLines(0, 9);
    w.w[i] := 1;
    ValidateNeededWraps(ALine1, w);
    AssertLineForWraps(Format('%s %d', [ATestName, i]), ALine1, w, True);
  end;

end;

procedure TTestWordWrap.TestWordWrapLineMapMerge;
var
  ANode1, ANode2: TSynWordWrapIndexPage;
  ALine1, ALine2: TSynWordWrapLineMap;
  ATestName: String;
  w: TExpWraps;
begin
  ANode1 := TSynWordWrapIndexPage(FTree.FindPageForLine(0, afmCreate).Page);
  ANode2 := TSynWordWrapIndexPage(FTree.FindPageForLine(100, afmCreate).Page);
  ALine1 := ANode1.SynWordWrapLineMapStore;
  ALine2 := ANode2.SynWordWrapLineMapStore;

  ATestName := 'Insert at start: no-offset => no-offset';
  InitLine(ALine1, w.init([2, 1, 3, 3, 1]));
  InitLine(ALine2, w.init([4, 5, 6]));
  ALine2.MoveLinesAtEndTo(ALine1, 0, 3);
  //ALine1.InsertLinesFromPage(ALine2, 0, 0, 3);
  AssertLineForWraps('', ALine1, w.init([4, 5, 6,   2, 1, 3, 3, 1,   1,1]));

  ATestName := 'Insert at start: no-offset => offset';
  InitLine(ALine1, w.init([1, 1, 3, 3, 1]));
  InitLine(ALine2, w.init([4, 5, 6]));
  ALine2.MoveLinesAtEndTo(ALine1, 0, 3);
  //ALine1.InsertLinesFromPage(ALine2, 0, 0, 3);
  AssertLineForWraps('', ALine1, w.init([4, 5, 6,   1, 1, 3, 3, 1,   1,1]));

  ATestName := 'Insert at start: offset => no offset';
  InitLine(ALine1, w.init([2, 1, 3, 3, 1]));
  InitLine(ALine2, w.init([1, 5, 6]));
  ALine2.MoveLinesAtEndTo(ALine1, 0, 3);
  //ALine1.InsertLinesFromPage(ALine2, 0, 0, 3);
  AssertLineForWraps('', ALine1, w.init([1, 5, 6,   2, 1, 3, 3, 1,   1,1]));

  ATestName := 'Insert at start: offset => offset';
  InitLine(ALine1, w.init([1, 1, 3, 3, 1]));
  InitLine(ALine2, w.init([1, 5, 6]));
  ALine2.MoveLinesAtEndTo(ALine1, 0, 3);
  //ALine1.InsertLinesFromPage(ALine2, 0, 0, 3);
  AssertLineForWraps('', ALine1, w.init([1, 5, 6,   1, 1, 3, 3, 1,   1,1]));


  ATestName := 'Insert at start: no-offset 2nd => no-offset';
  InitLine(ALine1, w.init([2, 1, 3, 3, 1]));
  InitLine(ALine2, w.init([4, 5, 6]));
  ALine2.MoveLinesAtEndTo(ALine1, 1, 2);
  //ALine1.InsertLinesFromPage(ALine2, 1, 0, 2);
  AssertLineForWraps('', ALine1, w.init([5, 6,   2, 1, 3, 3, 1,   1,1]));

  ATestName := 'Insert at start: no-offset 2nd => offset';
  InitLine(ALine1, w.init([1, 1, 3, 3, 1]));
  InitLine(ALine2, w.init([4, 5, 6]));
  ALine2.MoveLinesAtEndTo(ALine1, 1, 2);
  //ALine1.InsertLinesFromPage(ALine2, 1, 0, 2);
  AssertLineForWraps('', ALine1, w.init([5, 6,   1, 1, 3, 3, 1,   1,1]));

  ATestName := 'Insert at start: offset 2nd => no offset';
  InitLine(ALine1, w.init([2, 1, 3, 3, 1]));
  InitLine(ALine2, w.init([1, 5, 6]));
  ALine2.MoveLinesAtEndTo(ALine1, 1, 2);
  //ALine1.InsertLinesFromPage(ALine2, 1, 0, 2);
  AssertLineForWraps('', ALine1, w.init([5, 6,   2, 1, 3, 3, 1,   1,1]));

  ATestName := 'Insert at start: offset 2nd => offset';
  InitLine(ALine1, w.init([1, 1, 3, 3, 1]));
  InitLine(ALine2, w.init([1, 5, 6]));
  ALine2.MoveLinesAtEndTo(ALine1, 1, 2);
  //ALine1.InsertLinesFromPage(ALine2, 1, 0, 2);
  AssertLineForWraps('', ALine1, w.init([5, 6,   1, 1, 3, 3, 1,   1,1]));

  ATestName := 'Insert at start: offset 3rd => no offset';
  InitLine(ALine1, w.init([2, 1, 3, 3, 1]));
  InitLine(ALine2, w.init([1, 5, 6, 7]));
  ALine2.MoveLinesAtEndTo(ALine1, 2, 2);
  //ALine1.InsertLinesFromPage(ALine2, 2, 0, 2);
  AssertLineForWraps('', ALine1, w.init([6, 7,   2, 1, 3, 3, 1,   1,1]));

  ATestName := 'Insert at start: offset 3rd => offset';
  InitLine(ALine1, w.init([1, 1, 3, 3, 1]));
  InitLine(ALine2, w.init([1, 5, 6, 7]));
  ALine2.MoveLinesAtEndTo(ALine1, 2, 2);
  //ALine1.InsertLinesFromPage(ALine2, 2, 0, 2);
  AssertLineForWraps('', ALine1, w.init([6, 7,   1, 1, 3, 3, 1,   1,1]));


  ATestName := 'Insert at start: overlen';
  InitLine(ALine1, w.init([1, 1, 3, 3, 1]));
  InitLine(ALine2, w.init([4, 5, 6]));
  ALine2.MoveLinesAtEndTo(ALine1, 0, 4);
  //ALine1.InsertLinesFromPage(ALine2, 0, 0, 4);
  AssertLineForWraps(ATestName, ALine1, w.init([4, 5, 6, 1,   1, 1, 3, 3, 1,   1,1]));

  InitLine(ALine1, w.init([1, 1, 3, 3, 1]));
  InitLine(ALine2, w.init([4, 5, 6]));
  ALine2.MoveLinesAtEndTo(ALine1, 1, 4);
  //ALine1.InsertLinesFromPage(ALine2, 1, 0, 4);
  AssertLineForWraps(ATestName, ALine1, w.init([5, 6, 1, 1,   1, 1, 3, 3, 1,   1,1]));

  InitLine(ALine1, w.init([1, 1, 3, 3, 1]));
  InitLine(ALine2, w.init([1]));
  ALine2.MoveLinesAtEndTo(ALine1, 1, 4);
  //ALine1.InsertLinesFromPage(ALine2, 1, 0, 4);
  AssertLineForWraps(ATestName, ALine1, w.init([1, 1, 1, 1,   1, 1, 3, 3, 1,   1,1]));



  ATestName := 'Insert at end';
  InitLine(ALine1, w.init([1, 1, 3, 3, 1]));
  InitLine(ALine2, w.init([4, 5, 6]));
  ALine2.MoveLinesAtStartTo(ALine1, 2, 5);
  //ALine1.InsertLinesFromPage(ALine2, 0, 5, 3);
  AssertLineForWraps(ATestName, ALine1, w.init([1, 1, 3, 3, 1,   4, 5, 6,   1,1]));

  InitLine(ALine1, w.init([1, 1, 3, 3, 1]));
  InitLine(ALine2, w.init([4, 5, 6]));
  ALine2.MoveLinesAtStartTo(ALine1, 2, 6);
  //ALine1.InsertLinesFromPage(ALine2, 0, 6, 3);
  AssertLineForWraps(ATestName, ALine1, w.init([1, 1, 3, 3, 1,   1, 4, 5, 6,   1,1]));


  InitLine(ALine1, w.init([1, 1, 3, 3, 1]));
  InitLine(ALine2, w.init([1, 5, 6]));
  ALine2.MoveLinesAtStartTo(ALine1, 2, 5);
  //ALine1.InsertLinesFromPage(ALine2, 0, 5, 3);
  AssertLineForWraps(ATestName, ALine1, w.init([1, 1, 3, 3, 1,   1, 5, 6,   1,1]));

  InitLine(ALine1, w.init([1, 1, 3, 3, 1]));
  InitLine(ALine2, w.init([1, 5, 6]));
  ALine2.MoveLinesAtStartTo(ALine1, 2, 6);
  //ALine1.InsertLinesFromPage(ALine2, 0, 6, 3);
  AssertLineForWraps(ATestName, ALine1, w.init([1, 1, 3, 3, 1,   1, 1, 5, 6,   1,1]));

///////////////////////////////////////
  (* split node at none wrapping lines - ensure the none-wrap "WrappedExtraSums" are stripped *)
  ATestName := 'Insert at start: empty lines in the middle -> dest 2';
  InitLine(ALine1, w.init([4, 5]));
  InitLine(ALine2, w.init([2, 1, 1, 1, 1, 3]));
  ALine2.MoveLinesAtEndTo(ALine1, 3, 3);
  AssertLineForWraps('', ALine1, w.init([1, 1, 3, 4, 5,   1,1]));

  ATestName := 'Insert at start: empty lines in the middle -> dest 1';
  InitLine(ALine1, w.init([4]));
  InitLine(ALine2, w.init([2, 1, 1, 1, 1, 3]));
  ALine2.MoveLinesAtEndTo(ALine1, 3, 3);
  AssertLineForWraps('', ALine1, w.init([1, 1, 3, 4,   1,1]));

  ATestName := 'Insert at start: empty lines in the middle -> empty dest';
  InitLine(ALine1, w.init([]));
  InitLine(ALine2, w.init([2, 1, 1, 1, 1, 3]));
  ALine2.MoveLinesAtEndTo(ALine1, 3, 3);
  AssertLineForWraps('', ALine1, w.init([1, 1, 3,   1,1]));

  ATestName := 'Insert at start: empty lines in the middle -> dest 2 - with dest offset';
  InitLine(ALine1, w.init([1, 1, 4, 5]));
  InitLine(ALine2, w.init([2, 1, 1, 1, 1, 3]));
  ALine2.MoveLinesAtEndTo(ALine1, 3, 3);
  AssertLineForWraps('', ALine1, w.init([1, 1, 3, 1, 1, 4, 5,   1,1]));

  ATestName := 'Insert at start: empty lines in the middle -> dest 2 - with source offset';
  InitLine(ALine1, w.init([4, 5]));
  InitLine(ALine2, w.init([1, 1, 2, 1, 1, 1, 1, 3]));
  ALine2.MoveLinesAtEndTo(ALine1, 5, 3);
  AssertLineForWraps('', ALine1, w.init([1, 1, 3, 4, 5,   1,1]));



  ATestName := 'Insert at end : empty lines in the middle -> dest 2';
  InitLine(ALine1, w.init([4, 5]));
  InitLine(ALine2, w.init([2, 1, 1, 1, 1, 3]));
  ALine2.MoveLinesAtStartTo(ALine1, 3, 2);
  AssertLineForWraps('', ALine1, w.init([4, 5, 2, 1, 1,   1,1]));

  ATestName := 'Insert at end : empty lines in the middle -> dest 1';
  InitLine(ALine1, w.init([4]));
  InitLine(ALine2, w.init([2, 1, 1, 1, 1, 3]));
  ALine2.MoveLinesAtStartTo(ALine1, 3, 1);
  AssertLineForWraps('', ALine1, w.init([4, 2, 1, 1,   1,1]));

  ATestName := 'Insert at end : empty lines in the middle -> emyty dest';
  InitLine(ALine1, w.init([]));
  InitLine(ALine2, w.init([2, 1, 1, 1, 1, 3]));
  ALine2.MoveLinesAtStartTo(ALine1, 3, 0);
  AssertLineForWraps('', ALine1, w.init([2, 1, 1,   1,1]));

  ATestName := 'Insert at end : empty lines in the middle -> dest 2 - with dest offset';
  InitLine(ALine1, w.init([1, 1, 4, 5]));
  InitLine(ALine2, w.init([2, 1, 1, 1, 1, 3]));
  ALine2.MoveLinesAtStartTo(ALine1, 3, 4);
  AssertLineForWraps('', ALine1, w.init([1, 1, 4, 5, 2, 1, 1,   1,1]));

  ATestName := 'Insert at end : empty lines in the middle -> dest 2 - with source offset';
  InitLine(ALine1, w.init([4, 5]));
  InitLine(ALine2, w.init([1, 1, 2, 1, 1, 1, 1, 2]));
  ALine2.MoveLinesAtStartTo(ALine1, 5, 2);
  AssertLineForWraps('', ALine1, w.init([4, 5, 1, 1, 2, 1, 1,   1,1]));

end;

procedure TTestWordWrap.TestWordWrapLineMapMergeInvalidate;
var
  ANode1, ANode2: TSynWordWrapIndexPage;
  ALine1, ALine2: TSynWordWrapLineMap;
  ATestName: String;
  w: TExpWraps;

  procedure DoMoveLinesAtEndTo(const AName: String;
    const AWrapValues1, AWrapValues2: array of integer; AInvalLine: Integer; const AInvalDest: Boolean;
    ASourceStartLine, ALineCount: Integer;
    Exp: array of integer;  ExpInval: Integer
  );
  begin
    InitLine(ALine1, w.init(AWrapValues1));
    InitLine(ALine2, w.init(AWrapValues2));
    if AInvalDest then
      ALine1.InvalidateLines(AInvalLine, AInvalLine)
    else
      ALine2.InvalidateLines(AInvalLine, AInvalLine);
    ALine2.MoveLinesAtEndTo(ALine1, ASourceStartLine, ALineCount);
    AssertLineForWraps(AName, ALine1, w.init(Exp));
    AssertEquals(AName+' invalid', ExpInval, ALine1.FirstInvalidLine);
    AssertEquals(AName+' invalid', ExpInval, ALine1.LastInvalidLine);
  end;

  procedure DoMoveLinesAtStartTo(const AName: String;
    const AWrapValues1, AWrapValues2: array of integer; AInvalLine: Integer; const AInvalDest: Boolean;
    ASourceEndLine, ATargetStartLine: Integer;
    Exp: array of integer;  ExpInval: Integer
  );
  begin
    InitLine(ALine1, w.init(AWrapValues1));
    InitLine(ALine2, w.init(AWrapValues2));
    if AInvalDest then
      ALine1.InvalidateLines(AInvalLine, AInvalLine)
    else
      ALine2.InvalidateLines(AInvalLine, AInvalLine);
    ALine2.MoveLinesAtStartTo(ALine1, ASourceEndLine, ATargetStartLine);
    AssertLineForWraps(AName, ALine1, w.init(Exp));
    AssertEquals(AName+' invalid', ExpInval, ALine1.FirstInvalidLine);
    AssertEquals(AName+' invalid', ExpInval, ALine1.LastInvalidLine);
  end;

begin
  ANode1 := TSynWordWrapIndexPage(FTree.FindPageForLine(0, afmCreate).Page);
  ANode2 := TSynWordWrapIndexPage(FTree.FindPageForLine(100, afmCreate).Page);
  ALine1 := ANode1.SynWordWrapLineMapStore;
  ALine2 := ANode2.SynWordWrapLineMapStore;

  ATestName := 'Insert at start: target inval';
  InitLine(ALine1, w.init([2, 1, 3, 3, 1]));
  InitLine(ALine2, w.init([4, 5, 6]));
  ALine1.InvalidateLines(1, 1);
  ALine2.MoveLinesAtEndTo(ALine1, 0, 3);
  //ALine1.InsertLinesFromPage(ALine2, 0, 0, 3);
  AssertLineForWraps('', ALine1, w.init([4, 5, 6,   2, 1, 3, 3, 1,   1,1]));
  AssertEquals('invalid', 4, ALine1.FirstInvalidLine);
  AssertEquals('invalid', 4, ALine1.LastInvalidLine);

  ATestName := 'Insert at start: source inval';
  InitLine(ALine1, w.init([2, 1, 3, 3, 1]));
  InitLine(ALine2, w.init([4, 5, 6]));
  ALine2.InvalidateLines(1, 1);
  ALine2.MoveLinesAtEndTo(ALine1, 0, 3);
  //ALine1.InsertLinesFromPage(ALine2, 0, 0, 3);
  AssertLineForWraps('', ALine1, w.init([4, 5, 6,   2, 1, 3, 3, 1,   1,1]));
  AssertEquals('invalid', 1, ALine1.FirstInvalidLine);
  AssertEquals('invalid', 1, ALine1.LastInvalidLine);

  ATestName := 'Insert at start: source inval';
  InitLine(ALine1, w.init([2, 1, 3, 3, 1]));
  InitLine(ALine2, w.init([4, 5, 6]));
  ALine2.InvalidateLines(0, 1);
  ALine2.MoveLinesAtEndTo(ALine1, 1, 2);
  //ALine1.InsertLinesFromPage(ALine2, 1, 0, 2);
  AssertLineForWraps('', ALine1, w.init([5, 6,   2, 1, 3, 3, 1,   1,1]));
  AssertEquals('invalid', 0, ALine1.FirstInvalidLine);
  AssertEquals('invalid', 0, ALine1.LastInvalidLine);

  ATestName := 'Insert at start: source inval';
  InitLine(ALine1, w.init([2, 1, 3, 3, 1]));
  InitLine(ALine2, w.init([4, 5, 6]));
  ALine2.InvalidateLines(0, 1);
  ALine2.MoveLinesAtEndTo(ALine1, 2, 1);
  //ALine1.InsertLinesFromPage(ALine2, 2, 0, 1);
  AssertLineForWraps('', ALine1, w.init([6,   2, 1, 3, 3, 1,   1,1]));
  AssertEquals('invalid', -1, ALine1.FirstInvalidLine);
  AssertEquals('invalid', -1, ALine1.LastInvalidLine);

  ATestName := 'Insert at start: both inval';
  InitLine(ALine1, w.init([2, 1, 3, 3, 1]));
  InitLine(ALine2, w.init([4, 5, 6]));
  ALine1.InvalidateLines(1, 1);
  ALine2.InvalidateLines(2, 2);
  ALine2.MoveLinesAtEndTo(ALine1, 0, 3);
  //ALine1.InsertLinesFromPage(ALine2, 0, 0, 3);
  AssertLineForWraps('', ALine1, w.init([4, 5, 6,   2, 1, 3, 3, 1,   1,1]));
  AssertEquals('invalid', 2, ALine1.FirstInvalidLine);
  AssertEquals('invalid', 4, ALine1.LastInvalidLine);


  ATestName := 'Insert at end: source inval';
  InitLine(ALine1, w.init([1, 1, 3, 3, 1]));
  InitLine(ALine2, w.init([4, 5, 6]));
  ALine2.InvalidateLines(2, 2);
  ALine2.MoveLinesAtStartTo(ALine1, 2, 5);
  //ALine1.InsertLinesFromPage(ALine2, 0, 5, 3);
  AssertLineForWraps(ATestName, ALine1, w.init([1, 1, 3, 3, 1,   4, 5, 6,   1,1]));
  AssertEquals('invalid', 7, ALine1.FirstInvalidLine);
  AssertEquals('invalid', 7, ALine1.LastInvalidLine);



  ATestName := 'Insert from end to empty';
  InitLine(ALine1, w.init([1, 1, 3, 3, 1]));
  InitLine(ALine2, w.init([]));
  ALine1.MoveLinesAtEndTo(ALine2, 3, 3);
  AssertLineForWraps(ATestName, ALine2, w.init([3, 1, 1,  1,1,1]));

  ATestName := 'Insert from end to empty';
  InitLine(ALine1, w.init([1, 1, 3, 3, 1]));
  InitLine(ALine2, w.init([]));
  ALine1.MoveLinesAtEndTo(ALine2, 2, 3);
  AssertLineForWraps(ATestName, ALine2, w.init([3, 3, 1,  1,1,1]));

  ATestName := 'Insert from end to empty';
  InitLine(ALine1, w.init([1, 1, 3, 3, 1]));
  InitLine(ALine2, w.init([]));
  ALine1.MoveLinesAtEndTo(ALine2, 1, 3);
  AssertLineForWraps(ATestName, ALine2, w.init([1, 3, 3,  1,1,1]));

  ATestName := 'Insert from end to empty';
  InitLine(ALine1, w.init([1, 1, 3, 3, 1]));
  InitLine(ALine2, w.init([]));
  ALine1.MoveLinesAtEndTo(ALine2, 1, 4);
  AssertLineForWraps(ATestName, ALine2, w.init([1, 3, 3, 1,  1,1,1]));

  ATestName := 'Insert from after end to empty';
  InitLine(ALine1, w.init([1, 1, 3, 3, 1]));
  InitLine(ALine2, w.init([]));
  ALine1.MoveLinesAtEndTo(ALine2, 6, 3);
  AssertLineForWraps(ATestName, ALine2, w.init([1, 1, 1,  1,1,1]));


  ATestName := 'Insert from start to empty';
  InitLine(ALine1, w.init([1, 1, 3, 3, 1]));
  InitLine(ALine2, w.init([]));
  ALine1.MoveLinesAtStartTo(ALine2, 2, 0);
  AssertLineForWraps(ATestName, ALine2, w.init([1, 1, 3,    1,1,1]));

  ATestName := 'Insert from start to empty';
  InitLine(ALine1, w.init([2, 1, 3, 3, 1]));
  InitLine(ALine2, w.init([]));
  ALine1.MoveLinesAtStartTo(ALine2, 2, 0);
  AssertLineForWraps(ATestName, ALine2, w.init([2, 1, 3,    1,1,1,1]));

  ATestName := 'Insert from start to empty';
  InitLine(ALine1, w.init([1, 1, 3, 3, 1]));
  InitLine(ALine2, w.init([]));
  ALine1.MoveLinesAtStartTo(ALine2, 1, 0);
  AssertLineForWraps(ATestName, ALine2, w.init([1, 1,     1,1,1,1]));

  ATestName := 'Insert from start to empty';
  InitLine(ALine1, w.init([1, 1, 3, 3, 1]));
  InitLine(ALine2, w.init([]));
  ALine1.MoveLinesAtStartTo(ALine2, 2, 3);
  AssertLineForWraps(ATestName, ALine2, w.init([1, 1, 1,  1, 1, 3,    1,1,1]));


///////////////////////////////////////
  (* split node at none wrapping lines - ensure the none-wrap "WrappedExtraSums" are stripped *)
  DoMoveLinesAtEndTo('Insert at start: empty lines in the middle -> dest 2',
    [4, 5],  [2, 1, 1, 1, 1, 3],   1,  True,    3, 3,  {=>}  [1, 1, 3, 4, 5,   1,1],  4);

  DoMoveLinesAtEndTo('Insert at start: empty lines in the middle -> dest 2',
    [4, 5],  [2, 1, 1, 1, 1, 3],   1,  False,    3, 3, {=>}   [1, 1, 3, 4, 5,   1,1],  -1);
  DoMoveLinesAtEndTo('Insert at start: empty lines in the middle -> dest 2',
    [4, 5],  [2, 1, 1, 1, 1, 3],   2,  False,    3, 3, {=>}   [1, 1, 3, 4, 5,   1,1],  -1);
  DoMoveLinesAtEndTo('Insert at start: empty lines in the middle -> dest 2',
    [4, 5],  [2, 1, 1, 1, 1, 3],   3,  False,    3, 3, {=>}   [1, 1, 3, 4, 5,   1,1],  0);
  DoMoveLinesAtEndTo('Insert at start: empty lines in the middle -> dest 2',
    [4, 5],  [2, 1, 1, 1, 1, 3],   4,  False,    3, 3, {=>}   [1, 1, 3, 4, 5,   1,1],  1);


  DoMoveLinesAtEndTo('Insert at start: empty lines in the middle -> dest 2 - gap at source end',
    [4, 5],  [2, 1, 1, 1, 1, 3],   1,  True,    3, 4,  {=>}  [1, 1, 3, 1, 4, 5,   1,1],  5);

  DoMoveLinesAtEndTo('Insert at start: empty lines in the middle -> dest 2 - gap at source end',
    [4, 5],  [2, 1, 1, 1, 1, 3],   1,  False,    3, 4,  {=>}  [1, 1, 3, 1, 4, 5,   1,1],  -1);
  DoMoveLinesAtEndTo('Insert at start: empty lines in the middle -> dest 2 - gap at source end',
    [4, 5],  [2, 1, 1, 1, 1, 3],   4,  False,    3, 4,  {=>}  [1, 1, 3, 1, 4, 5,   1,1],  1);


  DoMoveLinesAtEndTo('Insert at start: empty lines in the middle -> dest 1',
    [4],  [2, 1, 1, 1, 1, 3],      1,  True,    3, 3,  {=>}  [1, 1, 3, 4,   1,1],  4);

  DoMoveLinesAtEndTo('Insert at start: empty lines in the middle -> dest 1',
    [4],  [2, 1, 1, 1, 1, 3],      1,  False,    3, 3, {=>}   [1, 1, 3, 4,   1,1],  -1);
  DoMoveLinesAtEndTo('Insert at start: empty lines in the middle -> dest 1',
    [4],  [2, 1, 1, 1, 1, 3],      3,  False,    3, 3, {=>}   [1, 1, 3, 4,   1,1],  0);


  DoMoveLinesAtEndTo('Insert at start: empty lines in the middle -> empty dest',
    [],  [2, 1, 1, 1, 1, 3],       1,  True,    3, 3,  {=>}  [1, 1, 3,   1,1],  4);

  DoMoveLinesAtEndTo('Insert at start: empty lines in the middle -> empty dest',
    [],  [2, 1, 1, 1, 1, 3],       1,  False,    3, 3, {=>}   [1, 1, 3,   1,1],  -1);
  DoMoveLinesAtEndTo('Insert at start: empty lines in the middle -> empty dest',
    [],  [2, 1, 1, 1, 1, 3],       4,  False,    3, 3, {=>}   [1, 1, 3,   1,1],  1);


  DoMoveLinesAtEndTo('Insert at start: empty lines in the middle -> dest 2 - with dest offset',
    [1,1,4, 5],  [2, 1, 1, 1, 1, 3],   1,  True,    3, 3,  {=>}  [1, 1, 3, 1,1,4, 5,   1,1],  4);

  DoMoveLinesAtEndTo('Insert at start: empty lines in the middle -> dest 2 - with dest offset',
    [1,1,4, 5],  [2, 1, 1, 1, 1, 3],   1,  False,   3, 3,  {=>}  [1, 1, 3, 1,1,4, 5,   1,1], -1);
  DoMoveLinesAtEndTo('Insert at start: empty lines in the middle -> dest 2 - with dest offset',
    [1,1,4, 5],  [2, 1, 1, 1, 1, 3],   4,  False,   3, 3,  {=>}  [1, 1, 3, 1,1,4, 5,   1,1],  1);


  DoMoveLinesAtEndTo('Insert at start: empty lines in the middle -> dest 2 - with source offset',
    [4, 5],  [1,1,2, 1, 1, 1, 1, 3],   1,  True,    5, 3,  {=>}  [1, 1, 3, 4, 5,   1,1],  4);

  DoMoveLinesAtEndTo('Insert at start: empty lines in the middle -> dest 2 - with source offset',
    [4, 5],  [1,1,2, 1, 1, 1, 1, 3],   1,  False,    5, 3,  {=>}  [1, 1, 3, 4, 5,   1,1],  -1);
  DoMoveLinesAtEndTo('Insert at start: empty lines in the middle -> dest 2 - with source offset',
    [4, 5],  [1,1,2, 1, 1, 1, 1, 3],   3,  False,    5, 3,  {=>}  [1, 1, 3, 4, 5,   1,1],  -1);
  DoMoveLinesAtEndTo('Insert at start: empty lines in the middle -> dest 2 - with source offset',
    [4, 5],  [1,1,2, 1, 1, 1, 1, 3],   6,  False,    5, 3,  {=>}  [1, 1, 3, 4, 5,   1,1],  1);





  DoMoveLinesAtStartTo('Insert at start: empty lines in the middle -> dest 2',
    [4, 5],  [2, 1, 1, 1, 1, 3],   1,  True,    3, 2,  {=>}  [4, 5, 2, 1, 1,   1,1],  1);

  DoMoveLinesAtStartTo('Insert at start: empty lines in the middle -> dest 2',
    [4, 5],  [2, 1, 1, 1, 1, 3],   1,  False,    3, 2,  {=>}  [4, 5, 2, 1, 1,   1,1],  3);
  DoMoveLinesAtStartTo('Insert at start: empty lines in the middle -> dest 2',
    [4, 5],  [2, 1, 1, 1, 1, 3],   4,  False,    3, 2,  {=>}  [4, 5, 2, 1, 1,   1,1],  -1);


  DoMoveLinesAtStartTo('Insert at start: empty lines in the middle -> dest 2 - gap at target end',
    [4, 5],  [2, 1, 1, 1, 1, 3],   1,  True,    3, 3,  {=>}  [4, 5, 1,  2, 1, 1,   1,1],  1);

  DoMoveLinesAtStartTo('Insert at start: empty lines in the middle -> dest 2 - gap at target end',
    [4, 5],  [2, 1, 1, 1, 1, 3],   1,  False,    3, 3,  {=>}  [4, 5, 1,  2, 1, 1,   1,1],  4);
  DoMoveLinesAtStartTo('Insert at start: empty lines in the middle -> dest 2 - gap at target end',
    [4, 5],  [2, 1, 1, 1, 1, 3],   4,  False,    3, 3,  {=>}  [4, 5, 1,  2, 1, 1,   1,1],  -1);


  DoMoveLinesAtStartTo('Insert at start: empty lines in the middle -> dest 1',
    [4],  [2, 1, 1, 1, 1, 3],   1,  True,    3, 1,  {=>}  [4, 2, 1, 1,   1,1],  1);

  DoMoveLinesAtStartTo('Insert at start: empty lines in the middle -> dest 1',
    [4],  [2, 1, 1, 1, 1, 3],   1,  False,    3, 1,  {=>}  [4, 2, 1, 1,   1,1],  2);
  DoMoveLinesAtStartTo('Insert at start: empty lines in the middle -> dest 1',
    [4],  [2, 1, 1, 1, 1, 3],   4,  False,    3, 1,  {=>}  [4, 2, 1, 1,   1,1],  -1);


// empty dest can not have invalid...
  //DoMoveLinesAtStartTo('Insert at start: empty lines in the middle -> empty dest',
  //  [],  [2, 1, 1, 1, 1, 3],   1,  True,    3, 0,  {=>}  [2, 1, 1,   1,1],  1);

  DoMoveLinesAtStartTo('Insert at start: empty lines in the middle -> empty dest',
    [],  [2, 1, 1, 1, 1, 3],   1,  False,    3, 0,  {=>}  [2, 1, 1,   1,1],  1);
  DoMoveLinesAtStartTo('Insert at start: empty lines in the middle -> empty dest',
    [],  [2, 1, 1, 1, 1, 3],   4,  False,    3, 0,  {=>}  [2, 1, 1,   1,1],  -1);


  DoMoveLinesAtStartTo('Insert at start: empty lines in the middle -> empty dest - gap',
    [],  [2, 1, 1, 1, 1, 3],   1,  True,    3, 2,  {=>}  [1,1, 2, 1, 1,   1,1],  1);

  DoMoveLinesAtStartTo('Insert at start: empty lines in the middle -> empty dest - gap',
    [],  [2, 1, 1, 1, 1, 3],   1,  False,    3, 2,  {=>}  [1,1, 2, 1, 1,   1,1],  3);
  DoMoveLinesAtStartTo('Insert at start: empty lines in the middle -> empty dest- gap',
    [],  [2, 1, 1, 1, 1, 3],   4,  False,    3, 2,  {=>}  [1,1, 2, 1, 1,   1,1],  -1);


  DoMoveLinesAtStartTo('Insert at start: empty lines in the middle -> dest 2 - with dest offset',
    [1,1, 4, 5],  [2, 1, 1, 1, 1, 3],   1,  True,    3, 4,  {=>}  [1,1, 4, 5, 2, 1, 1,   1,1],  1);

  DoMoveLinesAtStartTo('Insert at start: empty lines in the middle -> dest 2 - with dest offset',
    [1,1, 4, 5],  [2, 1, 1, 1, 1, 3],   1,  False,    3, 4,  {=>}  [1,1, 4, 5, 2, 1, 1,   1,1],  5);
  DoMoveLinesAtStartTo('Insert at start: empty lines in the middle -> dest 2 - with dest offset',
    [1,1, 4, 5],  [2, 1, 1, 1, 1, 3],   4,  False,    3, 4,  {=>}  [1,1, 4, 5, 2, 1, 1,   1,1],  -1);


  DoMoveLinesAtStartTo('Insert at start: empty lines in the middle -> dest 2 - with dest offset - gap',
    [1,1, 4, 5],  [2, 1, 1, 1, 1, 3],   1,  True,    3, 5,  {=>}  [1,1, 4, 5, 1, 2, 1, 1,   1,1],  1);

  DoMoveLinesAtStartTo('Insert at start: empty lines in the middle -> dest 2 - with dest offset - gap',
    [1,1, 4, 5],  [2, 1, 1, 1, 1, 3],   1,  False,    3, 5,  {=>}  [1,1, 4, 5, 1, 2, 1, 1,   1,1],  6);
  DoMoveLinesAtStartTo('Insert at start: empty lines in the middle -> dest 2 - with dest offset - gap',
    [1,1, 4, 5],  [2, 1, 1, 1, 1, 3],   4,  False,    3, 5,  {=>}  [1,1, 4, 5, 1, 2, 1, 1,   1,1],  -1);


  DoMoveLinesAtStartTo('Insert at start: empty lines in the middle -> dest 2 - with source offset',
    [4, 5],  [1,1, 2, 1, 1, 1, 1, 3],   1,  True,    5, 2,  {=>}  [4, 5, 1,1, 2, 1, 1,   1,1],  1);

  DoMoveLinesAtStartTo('Insert at start: empty lines in the middle -> dest 2 - with source offset',
    [4, 5],  [1,1, 2, 1, 1, 1, 1, 3],   1,  False,    5, 2,  {=>}  [4, 5, 1,1, 2, 1, 1,   1,1],  3);
  DoMoveLinesAtStartTo('Insert at start: empty lines in the middle -> dest 2 - with source offset',
    [4, 5],  [1,1, 2, 1, 1, 1, 1, 3],   6,  False,    5, 2,  {=>}  [4, 5, 1,1, 2, 1, 1,   1,1],  -1);


end;

procedure TTestWordWrap.TestWordWrapJoinWithSibling;
var
  CurWraps: TExpWraps;

  procedure DoTestAssert(AName: String; ExpNodeCnt: Integer);
  begin
    //debugln(AName); FTree.DebugDump ;
    AssertTreeForWraps(AName + ' Wrap', CurWraps);
    AssertEquals(AName + ' Cnt ', ExpNodeCnt, TreeNodeCount);
  end;

  procedure DoTestInit(AName: String; AFromVal, ALineCount, AnIncrease, ExpNodeCnt: Integer);
  begin
    FTree.Clear;
    CurWraps.InitFill(AFromVal, AFromVal+ALineCount-1, AnIncrease); // 4 pages
    FTree.AdjustForLinesInserted(0, ALineCount, 0);
    ValidateTreeWraps(CurWraps);

    DoTestAssert(Format('%s (Init %d, %d) Wrap', [AName, AFromVal, ALineCount]), ExpNodeCnt);
  end;

  procedure DoTestChgWrp(AName: String; AStartLine, ALineCount, AFromVal, AnIncrease, ExpNodeCnt: Integer);
  begin
    CurWraps.FillRange(AStartLine, ALineCount, AFromVal, AnIncrease);
    FTree.InvalidateLines(AStartLine, AStartLine + ALineCount - 1);
    ValidateTreeWraps(CurWraps);

    DoTestAssert(Format('%s (Changed %d, %d) Wrap', [AName, AStartLine, ALineCount]), ExpNodeCnt);
  end;

  procedure DoTestDelete(AName: String; AStartLine, ALineCount, ExpNodeCnt: Integer);
  begin
    FTree.AdjustForLinesDeleted(AStartLine, ALineCount,0);
    CurWraps.SpliceArray(AStartLine, ALineCount);

    DoTestAssert(Format('%s (Deleted %d, %d) Wrap', [AName, AStartLine, ALineCount]), ExpNodeCnt);
  end;

var
  OffsetAtStart, OffsetAtEnd, Node1Del, Node2Del, FinalNodeCount: Integer;
  N: String;
begin
  (* After "DoTestInit" each node should be filled to the max.
     So the test knows where each node begins
  *)
  FTree := CreateTree(10, 30, 4); // APageJoinSize, APageSplitSize, APageJoinDistance

  For OffsetAtStart := 0 to 3 do
  For OffsetAtEnd := 0 to 3 do  // RealEnd = NextNode.Startline-1 - x
  For Node1Del := 18 to 25 do
  For Node2Del := 18 to 25 do
  begin
    N := Format('Start: %d  End: %d  Del1: %d Del2: %d', [OffsetAtStart, OffsetAtEnd, Node1Del, Node2Del]);

    FinalNodeCount := 3;
    if (OffsetAtStart + OffsetAtEnd > 4) or   //  APageJoinDistance not met
       (Node1Del + OffsetAtEnd < 20) or (Node2Del + OffsetAtStart < 20)
    then
      FinalNodeCount := 4;

    DoTestInit  (N, 10, 4*30, 1,    4);  // Create 4 full nodes

    if OffsetAtStart > 0 then
      DoTestChgWrp(N, 90,              OffsetAtStart, 1, 0,   4);  // At Start of Last node
    if OffsetAtEnd > 0 then
      DoTestChgWrp(N, 90-OffsetAtEnd,  OffsetAtEnd,   1, 0,   4);  // At End of 2nd-Last node

    DoTestDelete(N, 60,          Node1Del,         4);
    DoTestDelete(N, 95-Node1Del, Node2Del,         FinalNodeCount);

    // Delete from 2nd node before 1st node
    DoTestInit  (N, 10, 4*30, 1,    4);  // Create 4 full nodes

    if OffsetAtStart > 0 then
      DoTestChgWrp(N, 90,              OffsetAtStart, 1, 0,   4);  // At Start of Last node
    if OffsetAtEnd > 0 then
      DoTestChgWrp(N, 90-OffsetAtEnd,  OffsetAtEnd,   1, 0,   4);  // At End of 2nd-Last node

    DoTestDelete(N, 95, Node2Del,         4);
    DoTestDelete(N, 60, Node1Del,         FinalNodeCount);

  end;
end;


procedure TTestWordWrap.TestWordWrapTreeInsertThenDelete;
var
  CurWraps: TExpWraps;
  InsPos, InsLen, DelPos, DelCount: Integer;
begin
  FTree.Free;
  FTree := CreateTree(2, 9, 4);
//  FTree := TSynLineMapAVLTree.Create(TSynWordWrapIndexPage, 2, 11, 4);
  for DelPos := 0 to 26 do
  for DelCount := 1 to Min(5, 27-DelPos) do
  for InsPos := 0 to 29 do
  for InsLen := 1 to 4 do begin
    FTree.Clear;

    // init
    CurWraps.InitFill(10, 10+26);
    FTree.AdjustForLinesInserted(0, 27, 0);
    ValidateTreeWraps(CurWraps);
    //FTree.DebugDump;
    AssertTreeForWraps(Format('Before ins at pos %d Len %d', [InsPos, InsLen]), CurWraps);

    // ins
    CurWraps.Join(FillArray(500, 499+InsLen), InsPos);
    FTree.AdjustForLinesInserted(InsPos, InsLen, 0);
    //FTree.DebugDump;
    ValidateTreeWraps(CurWraps);
    //FTree.DebugDump;
    AssertTreeForWraps(Format('After ins at pos %d Len %d ins at pos %d Len %d', [DelPos, DelCount, InsPos, InsLen]), CurWraps);

    // del
    FTree.AdjustForLinesDeleted(DelPos, DelCount, 0);
    CurWraps.SpliceArray(DelPos, DelCount);
    AssertTrue(Format('valid After del at pos %d Len %d ins at pos %d Len %d', [DelPos, DelCount, InsPos, InsLen]),
      not FTree.NeedsValidation);
    AssertTreeForWraps(Format('After del at pos %d Len %d ins at pos %d Len %d', [DelPos, DelCount, InsPos, InsLen]), CurWraps);
  end;
end;

procedure TTestWordWrap.TestWordWrapTreeDeleteThenInsert;
var
  CurWraps: TExpWraps;
  InsPos, InsLen, DelPos, DelCount: Integer;
begin
  FTree.Free;
  FTree := CreateTree(2, 9, 4);
//  FTree := TSynLineMapAVLTree.Create(TSynWordWrapIndexPage, 2, 11, 4);
  for DelPos := 0 to 26 do
  for DelCount := 1 to Min(5, 27-DelPos) do
  for InsPos := 0 to 29 do
  for InsLen := 1 to 4 do begin
//if  (InsPos<>15) or (InsLen<>1) or (DelPos<>0) or (DelCount<>1) then continue;
    FTree.Clear;

    // init
    CurWraps.InitFill(10, 10+26);
    FTree.AdjustForLinesInserted(0, 27, 0);
    ValidateTreeWraps(CurWraps);
    //FTree.DebugDump;
    AssertTreeForWraps(Format('Before del at pos %d Len %d ins at pos %d Len %d', [DelPos, DelCount, InsPos, InsLen]), CurWraps);

    // del
    FTree.AdjustForLinesDeleted(DelPos, DelCount, 0);
    CurWraps.SpliceArray(DelPos, DelCount);
    AssertTrue(Format('valid After del at pos %d Len %d ins at pos %d Len %d', [DelPos, DelCount, InsPos, InsLen]),
      not FTree.NeedsValidation);
    AssertTreeForWraps(Format('After del at pos %d Len %d ins at pos %d Len %d', [DelPos, DelCount, InsPos, InsLen]), CurWraps);

    // ins
    FTree.AdjustForLinesInserted(InsPos, InsLen, 0);
    CurWraps.Join(FillArray(500, 499+InsLen), InsPos);
    //FTree.DebugDump;
    ValidateTreeWraps(CurWraps);
    //FTree.DebugDump;
    AssertTreeForWraps(Format('After del/ins : del at pos %d Len %d ins at pos %d Len %d', [DelPos, DelCount, InsPos, InsLen]), CurWraps);
  end;
end;

{ TTestWordWrapPluginBase }

procedure TTestWordWrapPluginBase.ClearCaret;
begin
  SynEdit.CaretXY := Point(2,2);
  SynEdit.CaretXY := Point(1,1);
end;

function TTestWordWrapPluginBase.GetTreeNodeHolder(AIndex: Integer
  ): TSynEditLineMapPageHolder;
begin
  Result := FWordWrap.FLineMapView.Tree.FirstPage;
  while (AIndex > 0) and Result.HasPage do begin
    dec(AIndex);
    Result := Result.Next;
  end;

end;

procedure TTestWordWrapPluginBase.SetCaret(SourcePt: TPointType; APos: TPoint);
begin
  case SourcePt of
    ptViewed:          SynEdit.CaretObj.ViewedLineCharPos := Apos;
    ptAlternateViewed: SynEdit.CaretObj.ViewedLineCharPos := Apos;
    ptPhys:            SynEdit.CaretObj.LineCharPos       := Apos;
    ptLog:             SynEdit.CaretObj.LineBytePos       := APos;
  end;
end;

procedure TTestWordWrapPluginBase.TestCaret(AName: String;SourcePt, ExpPt: TPointType;
  AnExp: TPoint; AnExpOffs: Integer);
var
  got: TPoint;
  src, dest: String;
begin
  case ExpPt of
    ptViewed:          got := SynEdit.CaretObj.ViewedLineCharPos;
    ptAlternateViewed: got := SynEdit.CaretObj.ViewedLineCharPos;
    ptPhys:            got := SynEdit.CaretObj.LineCharPos;
    ptLog:             got := SynEdit.CaretObj.LineBytePos;
  end;
  writestr(src, SourcePt);
  writestr(dest, ExpPt);
  AssertEquals(Format('%s (%s -> %s)', [AName, src, dest]), AnExp, got);
  if (ExpPt = ptLog) and (AnExpOffs >= 0) then
    AssertEquals(Format('%s (%s -> %s) Offs: ', [AName, src, dest]), AnExpOffs, SynEdit.CaretObj.BytePosOffset);
end;

class procedure TTestWordWrapPluginBase.AssertEquals(const AMessage: string;
  Expected, Actual: TPoint);
begin
  AssertEquals(AMessage, dbgs(Expected), dbgs(Actual));
end;

procedure TTestWordWrapPluginBase.AddLines(AFirstLineIdx, ACount,
  ALen: Integer; AnID: String; SkipBeginUpdate: Boolean;
  AReplaceExisting: Boolean);
var
  i, j: Integer;
  l: String;
begin
  if not SkipBeginUpdate then SynEdit.BeginUpdate;
  for i := 0 to ACount - 1 do begin
    l := '';
    j := 0;
    while Length(l) < ALen do begin
      l := l + copy(AnID+'_'+IntToStr(i)+'_'+IntToStr(j) + '            ',1,12);
      inc(j);
    end;
    l := copy(l, 1, ALen);
    if AReplaceExisting then
      SynEdit.Lines[AFirstLineIdx + i] := l
    else
      SynEdit.Lines.Insert(AFirstLineIdx + i, l);
  end;
  if not SkipBeginUpdate then SynEdit.EndUpdate;
end;

procedure TTestWordWrapPluginBase.InternalCheckLine(AName: String;
  dsp: TLazSynDisplayView; ALine: TLineIdx; AExpTextStart: String;
  NoTrim: Boolean);
var
  gotRealLine: TLineIdx;
  gotStartPos, GotLineLen: Integer;
  gotTokenOk: Boolean;
  gotToken: TLazSynDisplayTokenInfo;
  gotText: PChar;
  s: String;
begin
  dsp.SetHighlighterTokensLine(ALine, gotRealLine, gotStartPos, GotLineLen);
  gotTokenOk := dsp.GetNextHighlighterToken(gotToken);
  if gotTokenOk then
    gotText := gotToken.TokenStart
  else
    gotText := '';

  if AExpTextStart = '' then begin
    AssertEquals(AName, '', gotText);
    AssertEquals(AName, 0, GotLineLen);
    exit;
  end
  else
  if gotText = '' then begin
    AssertTrue(AName, False);
    exit;
  end;

  if NoTrim then
    s := copy(gotText, 1, Length(AExpTextStart))
  else
    s := copy(Trim(gotText), 1, Length(AExpTextStart));
  if not(AExpTextStart = s) then begin
    debugln(['Failed ', AName, ' ', ALine, ':']);
    DebugLn(['GOT: "', StringReplace(gotText, #9, '#9', [rfReplaceAll]), '"']);
    DebugLn(['EXP: "', StringReplace(AExpTextStart, #9, '#9', [rfReplaceAll]), '"']);
  end;
  AssertEquals(AName, AExpTextStart, s);
end;

procedure TTestWordWrapPluginBase.CheckLine(AName: String; ALine: TLineIdx;
  AExpTextStart: String; NoTrim: Boolean);
var
  v: TSynEditStringsLinked;
  dsp: TLazSynDisplayView;
begin
  v := SynEdit.TextViewsManager.SynTextView[SynEdit.TextViewsManager.Count - 1];
  dsp := v.DisplayView;
  dsp.InitHighlighterTokens(nil);
  try
    InternalCheckLine(AName, dsp, ALine, AExpTextStart, NoTrim);
  finally
    dsp.FinishHighlighterTokens;
  end;
end;

procedure TTestWordWrapPluginBase.CheckLines(AName: String;
  AStartLine: TLineIdx; AExpTextStart: array of String; NoTrim: Boolean);
var
  v: TSynEditStringsLinked;
  dsp: TLazSynDisplayView;
  i, gotStartPos, GotLineLen: Integer;
  gotTokenOk: Boolean;
  gotToken: TLazSynDisplayTokenInfo;
  s: String;
  gotRealLine: TLineIdx;
begin
  v := SynEdit.TextViewsManager.SynTextView[SynEdit.TextViewsManager.Count - 1];
  dsp := v.DisplayView;
  dsp.InitHighlighterTokens(nil);
  try
    try
      for i := 0 to Length(AExpTextStart)-1 do
        InternalCheckLine(AName, dsp, AStartLine+i, AExpTextStart[i], NoTrim);
    except
      dsp.FinishHighlighterTokens;
      dsp.InitHighlighterTokens(nil);
      for i := 0 to Length(AExpTextStart)-1 do begin
        dsp.SetHighlighterTokensLine(AStartLine+i, gotRealLine, gotStartPos, GotLineLen);
        s := '';
        while dsp.GetNextHighlighterToken(gotToken) and (gotToken.TokenLength > 0) do
          s := s + copy(gotToken.TokenStart, 1, gotToken.TokenLength);
        debugln('Line %d (real %d): "%s" %d/%d start %d',
          [AStartLine+i, gotRealLine, StringReplace(s, #9, '#9', [rfReplaceAll]), length(s), GotLineLen, gotStartPos]);
      end;
      raise;
    end;
  finally
    dsp.FinishHighlighterTokens;
  end;
end;

procedure TTestWordWrapPluginBase.CheckLine(AName: String;
  AExpLine: TTestWrapLineInfo);
begin
  CheckLine(AName, AExpLine.ViewedIdx, AExpLine.TextStartMatch, AExpLine.NoTrim);
end;

procedure TTestWordWrapPluginBase.CheckLines(AName: String;
  AExpLines: TTestViewedLineRangeInfo);
var
  i: Integer;
  n: TSynEditLineMapPageHolder;
begin
  for i := 0 to length(AExpLines) - 1 do begin
    CheckLine(Format('%s (%d)', [AName, i]), AExpLines[i]);
    CheckLineIndexMapping(Format('%s (%d)', [AName, i]), AExpLines[i].TextIdx, AExpLines[i].ViewedTopIdx, AExpLines[i].ViewedBottomIdx);
  end;
  if TheTree <> nil then begin
    n := TheTree.FirstPage;
    if n.HasPage then
      CheckTree(AName+' (TreeCheck)', n.Page, n.StartLine, 0, SynEdit.Lines.Count-1);
  end;
end;

procedure TTestWordWrapPluginBase.CheckXyMap(AName: String; APhysTExtXY,
  AViewedXY: TPoint; OnlyViewToText: Boolean);
var
  v: TSynEditStringsLinked;
  GotTextXY, GotViewXY: TPoint;
begin
  v := SynEdit.TextViewsManager.SynTextView[SynEdit.TextViewsManager.Count - 1];
  GotTextXY := v.ViewXYToTextXY(AViewedXY);
  GotViewXY := v.TextXYToViewXY(APhysTExtXY);

  AssertTrue(Format('%s: Viewed %s to Text %s (exp) => got %s', [AName, dbgs(AViewedXY), dbgs(APhysTExtXY), dbgs(GotTextXY)]),
             (GotTextXY.x = APhysTExtXY.x) and (GotTextXY.y = APhysTExtXY.y) );
  if not OnlyViewToText then
  AssertTrue(Format('%s: Text %s to viewed %s (exp) => got %s', [AName, dbgs(APhysTExtXY), dbgs(AViewedXY), dbgs(GotViewXY)]),
             (GotViewXY.x = AViewedXY.x) and (GotViewXY.y = AViewedXY.y) );
end;

procedure TTestWordWrapPluginBase.CheckXyMap(AName: String; APhysTExtX,
  APhysTExtY, AViewedX, AViewedY: integer; OnlyViewToText: Boolean);
begin
  CheckXyMap(AName, Point(APhysTExtX, APhysTExtY), Point(AViewedX, AViewedY), OnlyViewToText);
end;

procedure TTestWordWrapPluginBase.CheckXyMap(AName: String;
  APoints: TPointSpecs);
var
  StartP, TestP: TPointType;
begin
  CheckXyMap(AName + 'XyMap', APoints.xy[ptPhys], APoints.xy[ptViewed]);
  if APoints.xy[ptAlternateViewed].x > 0 then
    CheckXyMap(AName+ 'XyMap(a)', APoints.xy[ptPhys], APoints.xy[ptAlternateViewed], True);


  for TestP in TPointType do begin
    if TestP = ptAlternateViewed then
      continue;

    ClearCaret;
    SynEdit.CaretXY := APoints.XY[ptPhys];
    TestCaret(AName, ptPhys, TestP, APoints.XY[TestP], APoints.LogOffs);

    if APoints.LogOffs <= 0 then begin
      ClearCaret;
      SynEdit.LogicalCaretXY := APoints.XY[ptLog];
      TestCaret(AName, ptLog, TestP, APoints.XY[TestP], APoints.LogOffs);
    end;

    AName := AName + ' [CaretObj] ';
    for StartP in TPointType do begin
      if APoints.xy[StartP].x <= 0 then
        Continue;
    if (StartP = ptAlternateViewed) then // and (TestP = ptViewed) then
      continue;
    if (StartP = ptLog) and (APoints.LogOffs > 0) then
      continue;

      ClearCaret;
      SetCaret(StartP, APoints.XY[StartP]);
      TestCaret(AName, StartP, TestP, APoints.XY[TestP], APoints.LogOffs);
    end;
  end;

end;

procedure TTestWordWrapPluginBase.CheckXyMap(AName: String;
  APoints: TPointSpecs; ATestCommands: array of TCommandAndPointSpecs);
var
  i: Integer;
  StartP, TestP: TPointType;
  n: string;
  c: TSynEditorCommand;
begin
  CheckXyMap(AName+'(p)', APoints);

  for i := 0 to Length(ATestCommands) - 1 do begin
    if not ATestCommands[i].RunOnlyIf then
      Continue;
    n := AName+'(p'+IntToStr(i)+')';
    CheckXyMap(n, ATestCommands[i].Exp);

    for StartP in TPointType do begin
      if APoints.xy[StartP].x <= 0 then
        Continue;
      if (StartP = ptAlternateViewed) then // and (TestP = ptViewed) then
        continue;
      if (StartP = ptLog) and (APoints.LogOffs > 0) then
        continue;

      for TestP in TPointType do begin
        if TestP = ptAlternateViewed then
          continue;
        ClearCaret;
        SetCaret(StartP, APoints.XY[StartP]);
        for c in ATestCommands[i].Cmd do
          SynEdit.ExecuteCommand(c, '', nil);

        TestCaret(n, StartP, TestP, ATestCommands[i].Exp.XY[TestP], ATestCommands[i].Exp.LogOffs);
      end;
    end;
  end;
end;

procedure TTestWordWrapPluginBase.CheckLineIndexMapping(AName: String;
  ATextIdx, AViewTopIdx, AViewBottomIdx: TLineIdx);
var
  v: TSynEditStringsLinked;
  i: TLineIdx;
  dv: TLazSynDisplayView;
  r: TLineRange;
begin
  v := SynEdit.TextViewsManager.SynTextView[SynEdit.TextViewsManager.Count - 1];
  dv := v.DisplayView;

  AssertEquals(AName + ' TextToViewIndex', AViewTopIdx, v.TextToViewIndex(ATextIdx));
  for i := AViewTopIdx to AViewBottomIdx do
    AssertEquals(AName + ' ViewToTextIndex', ATextIdx, v.ViewToTextIndex(i));

  r := dv.TextToViewIndex(ATextIdx);
  AssertEquals(AName + 'DispView.TextToViewIndex Top', AViewTopIdx, r.Top);
  AssertEquals(AName + 'DispView.TextToViewIndex Bottom', AViewBottomIdx, r.Bottom);
  for i := AViewTopIdx to AViewBottomIdx do begin
    AssertEquals(AName + ' ViewToTextIndex', ATextIdx, dv.ViewToTextIndex(i));

    AssertEquals(AName + ' ViewToTextIndexEx', ATextIdx, dv.ViewToTextIndexEx(i, r));
    AssertEquals(AName + ' ViewToTextIndexEx Top', AViewTopIdx, r.Top);
    AssertEquals(AName + ' ViewToTextIndexEx Bottom', AViewBottomIdx, r.Bottom);
  end;
end;

function TTestWordWrapPluginBase.TheTree: TSynLineMapAVLTree;
begin
  Result := nil;
  if FWordWrap = nil then
    exit;
  Result := FWordWrap.FLineMapView.Tree;
end;

procedure TTestWordWrapPluginBase.ReCreateEdit(ADispWidth: Integer);
begin
  TearDown;
  SetUp;
  SetSynEditWidth(ADispWidth);
end;

procedure TTestWordWrapPluginBase.SetUp;
begin
  inherited SetUp;
  FWordWrap := TLazSynEditLineWrapPlugin.Create(SynEdit);
end;

procedure TTestWordWrapPluginBase.TearDown;
begin
  inherited TearDown;
end;

{ TTestWordWrapPlugin }

procedure TTestWordWrapPlugin.TestEditorWrap;
var
  SkipTab, AllowPastEOL, KeepX: Boolean;
begin
  SynEdit.Options := [];
  SynEdit.TabWidth := 4;

  SetLines([
    'abc def ' + 'ABC DEFG ' + 'XYZ',
    //'A'  #9'B'  #9'C ' + 'DEF G'#9'H'  #9 + ''   #9   #9'xy',
    'A'#9'B'#9'C ' + 'DEF G'#9'H'#9 + #9#9'xy',
    'äää ööö ' + 'ÄÄÄ ÖÖÖ ' + 'ÜÜÜ',
    '999'
  ]);

  SetSynEditWidth(10);
  CheckLines('', 0, [
    'abc def ',
    'ABC DEFG ',
    'XYZ',
    'A'#9'B'#9'C ',
    'DEF G'#9'H'#9,
    #9#9'xy',
    'äää ööö ',
    'ÄÄÄ ÖÖÖ ',
    'ÜÜÜ',
    '999'
  ], True);

  CheckLines('', ViewedExp(0, [
    l(0, 0, 'abc def '),
    l(0, 1, 'ABC DEFG '),
    l(0, 2, 'XYZ'),
    l(1, 0, 'A'#9'B'#9'C '),
    l(1, 1, 'DEF G'#9'H'#9),
    l(1, 2, #9#9'xy'),
    l(2, 0, 'äää ööö '),
    l(2, 1, 'ÄÄÄ ÖÖÖ '),
    l(2, 2, 'ÜÜÜ'),
    l(3, 0, '999')
  ], tTrue));


  //CheckXyMap('', pt(4,3,   21,1,   21,1); // after "Z" EOL
  for AllowPastEOL in boolean do
  for KeepX in boolean do
  for SkipTab in boolean do begin
    if AllowPastEOL
    then SynEdit.Options := SynEdit.Options + [eoScrollPastEol]
    else SynEdit.Options := SynEdit.Options - [eoScrollPastEol];
    if SkipTab
    then SynEdit.Options2 := SynEdit.Options2 + [eoCaretSkipTab]
    else SynEdit.Options2 := SynEdit.Options2 - [eoCaretSkipTab];
    if KeepX
    then SynEdit.Options := SynEdit.Options + [eoKeepCaretX]
    else SynEdit.Options := SynEdit.Options - [eoKeepCaretX];

    FWordWrap.CaretWrapPos := wcpEOL;
    CheckXyMap('wcpEOL', p( 1,1,           1,1,    1,1),
                        [c(ecRight,                         2,1,   2,1,    2,1,0),
                         c(ecDown,                          2,2,  10,1,   10,1,0),
                         c([ecDown,ecDown],                 2,3,  19,1,   19,1,0),
                         c([ecDown,ecDown, ecRight],        3,3,  20,1,   20,1,0),
                         c([ecDown,ecDown, ecRight,ecDown], 2,4,   2,2,    2,2,0, SkipTab),
                         c([ecDown,ecDown, ecRight,ecDown], 3,4,   3,2,    2,2,1, not SkipTab),
                         c([ecDown,ecDown,ecDown],          1,4,   1,2,    1,2,0, KeepX),
                         c([ecDown,ecDown,ecDown],          2,4,   2,2,    2,2,0, not KeepX),
                         c([ecDown,ecDown,ecDown,ecDown],   2,5,  12,2,    8,2,0)
                        ]);
    CheckXyMap('wcpEOL', p( 2,1,           2,1,    2,1),
                        [c(ecDown,                          2,2,  10,1,   10,1,0)
                        ]);
    CheckXyMap('wcpEOL', p( 7,1,           7,1,    7,1), // after "e"
                        [c([ecColSelDown],              7,4,   7,2,   4,2,1,  not SkipTab ),    // A#9B#|9  // 1 in tab
                         c([ecColSelDown],              6,4,   6,2,   4,2,0,  SkipTab ),    // A#9B|#9  // 1 in tab
                         c([ecColSelDown,ecColSelDown], 7,7,   7,3,  12,3,0,  (not SkipTab) or KeepX ),
                         c([ecColSelDown,ecColSelDown], 6,7,   6,3,  10,3,0,  SkipTab and not KeepX ),
                         c([ecColSelDown,ecColSelDown,ecColSelDown], 4,10,   4,4,  4,4,0, not AllowPastEOL),
                         c([ecColSelDown,ecColSelDown,ecColSelDown], 7,10,   7,4,  7,4,0, AllowPastEOL and ((not SkipTab) or KeepX) )
                        ]);
    CheckXyMap('wcpEOL', p( 8,1,           8,1,    8,1));
    if AllowPastEOL then
    CheckXyMap('wcpEOL', p( 9,1,   1,2,    9,1,    9,1), // def |
                        [c([ecDown],                        9,2,  17,1,   17,1,0), // DEFG|
                         c([ecDown,ecDown],                 9,3,  26,1,   26,1,0), // XYZ     |
                         c([ecDown,ecDown,ecDown],          9,4,   9,2,    5,2,0), // A#9B#9|C
                         c([ecDown,ecDown,ecDown,ecDown],   8,5,  18,2,   14,2,0,  SkipTab),     // G#9|H#9
                         c([ecDown,ecDown,ecDown,ecDown],   9,5,  19,2,   14,2,1,  not SkipTab), // G#9H#|9 //in #9
                         c([ecDown,ecDown,ecDown,ecDown,ecDown],   9,6,  29,2,   17,2,0,  (not SkipTab) or KeepX), // before x
                         c([ecDown,ecDown,ecDown,ecDown,ecDown],   5,6,  25,2,   16,2,0,  SkipTab       and not KeepX),  // in tab, before x
                         c([ecDown,ecDown,ecDown,ecDown,ecDown,ecDown],   9,7,   9,3,   15,3,0,  (not SkipTab) or KeepX),
                         c([ecDown,ecDown,ecDown,ecDown,ecDown,ecDown],   5,7,   5,3,    8,3,0,  SkipTab and not KeepX)
                        ])
    else // not AllowPastEOL then
    CheckXyMap('wcpEOL', p( 9,1,   1,2,    9,1,    9,1), // after " " / still on line 1
                        [c([ecDown],                        9,2,  17,1,   17,1,0),             // DEFG|
                         c([ecDown,ecDown],                 4,3,  21,1,   21,1,0),             // XYZ|
                         c([ecDown,ecDown,ecDown],          9,4,   9,2,    5,2,0,  KeepX),     // A#9B#9|C
                         c([ecDown,ecDown,ecDown],          4,4,   4,2,    2,2,2,  (not KeepX) and (not SkipTab) ), // A#|9B#9C //in tab
                         c([ecDown,ecDown,ecDown],          2,4,   2,2,    2,2,0,  (not KeepX) and SkipTab)  // A#|9B#9C //in tab
                        ]);
    CheckXyMap('wcpEOL', p( 2,2,          10,1,   10,1));
    CheckXyMap('wcpEOL', p( 4,2,          12,1,   12,1), // ABC| DE
                         [c([ecColSelDown],              2,5,  12,2,   8,2,0),             // D|EF G#9
                          c([ecColSelDown,ecColSelDown], 4,8,  12,3,   21,3,0),
                          c([ecColSelDown,ecColSelDown,ecColSelDown], 4,10,  4,4,   4,4,0,  not AllowPastEOL)
                         ]);
    CheckXyMap('wcpEOL', p( 9,2,          17,1,   17,1)); // after "G"
    CheckXyMap('wcpEOL', p(10,2,   1,3,   18,1,   18,1)); // after " "
    CheckXyMap('wcpEOL', p( 2,3,          19,1,   19,1)); // after "X"
    CheckXyMap('wcpEOL', p( 4,3,          21,1,   21,1)); // after "Z" EOL
    if AllowPastEOL then
    CheckXyMap('wcpEOL', p( 5,3,          22,1,   22,1)); // at EOL + 1

    CheckXyMap('wcpEOL', p( 1,4,           1,2,    1,2),
                        [c([ecDown],                  2,5,  12,2,   8,2,0),
                         c([ecDown,ecDown],           5,6,  25,2,  16,2,0,  SkipTab),
                         c([ecDown,ecDown],           2,6,  22,2,  15,2,1,  not SkipTab),
                         c([ecDown,ecDown,ecDown],    1,7,   1,3,   1,3,0,  KeepX),
                         c([ecDown,ecDown,ecDown],    5,7,   5,3,   8,3,0,  (SkipTab) and not KeepX)
                        ]);
    CheckXyMap('wcpEOL', p( 2,4,           2,2,    2,2, 0)); // before tab
    if not SkipTab then
    CheckXyMap('wcpEOL', p( 3,4,           3,2,    2,2, 1)); // 1 inside tab
    CheckXyMap('wcpEOL', p( 5,4,           5,2,    3,2, 0)); // after tab
    CheckXyMap('wcpEOL', p( 9,4,           9,2,    5,2)); // after tab, before "C"
    CheckXyMap('wcpEOL', p(10,4,          10,2,    6,2)); // after "C"
    CheckXyMap('wcpEOL', p(11,4,   1,5,   11,2,    7,2)); // after " "
    CheckXyMap('wcpEOL', p( 2,5,          12,2,    8,2)); // after "D"
    CheckXyMap('wcpEOL', p( 8,5,          18,2,   14,2)); // after "H"
    if not SkipTab then
    CheckXyMap('wcpEOL', p( 9,5,          19,2,   14,2, 1)); // in #9
    if not SkipTab then
    CheckXyMap('wcpEOL', p(10,5,          20,2,   14,2, 2)); // in #9
    CheckXyMap('wcpEOL', p(11,5,   1,6,   21,2,   15,2)); // after #9
    if not SkipTab then
    CheckXyMap('wcpEOL', p( 2,6,          22,2,   15,2, 1)); // in #9 / next line
    CheckXyMap('wcpEOL', p( 5,6,          25,2,   16,2)); // after 1st #9 / next line
    CheckXyMap('wcpEOL', p(10,6,          30,2,   18,2)); // after "x"
    CheckXyMap('wcpEOL', p(11,6,          31,2,   19,2)); // after "z" EOL

    CheckXyMap('wcpEOL', p( 1,7,           1,3,    1,3));
    CheckXyMap('wcpEOL', p( 2,7,           2,3,    3,3));
    CheckXyMap('wcpEOL', p( 8,7,           8,3,   14,3)); // after "ö"
    CheckXyMap('wcpEOL', p( 9,7,   1,8,    9,3,   15,3)); // after " "
    CheckXyMap('wcpEOL', p( 2,8,          10,3,   17,3)); // after "Ä"
    CheckXyMap('wcpEOL', p( 9,8,   1,9,   17,3,   29,3)); // after " "

    FWordWrap.CaretWrapPos := wcpBOL;
    CheckXyMap('wcpBOL', p( 1,1,           1,1,    1,1),
                        [c(ecRight,                         2,1,   2,1,    2,1,0),
                         c(ecDown,                          1,2,   9,1,    9,1,0),
                         c([ecDown,ecDown],                 1,3,  18,1,   18,1,0),
                         c([ecDown,ecDown, ecRight,ecRight],        3,3,  20,1,   20,1,0),
                         c([ecDown,ecDown, ecRight,ecRight,ecDown], 2,4,   2,2,    2,2,0, SkipTab),
                         c([ecDown,ecDown, ecRight,ecRight,ecDown], 3,4,   3,2,    2,2,1, not SkipTab),
                         c([ecDown,ecDown,ecDown],          1,4,   1,2,    1,2,0),
                         c([ecDown,ecDown,ecDown,ecDown],   1,5,  11,2,    7,2,0)
                        ]);
    CheckXyMap('wcpBOL', p( 2,1,           2,1,    2,1),
                        [c(ecDown,                          2,2,  10,1,   10,1,0)
                        ]);
    CheckXyMap('wcpBOL', p( 7,1,           7,1,    7,1)); // after "e"
    CheckXyMap('wcpBOL', p( 8,1,           8,1,    8,1));
    CheckXyMap('wcpBOL', p( 1,2,   9,1,    9,1,    9,1)); // after " " / still on line 1
    CheckXyMap('wcpBOL', p( 2,2,          10,1,   10,1));
    CheckXyMap('wcpBOL', p( 9,2,          17,1,   17,1), // after "G"
                        [c([ecUp],                          8,1,   8,1,   8,1,0),
                         c([ecUp, ecDown],                  9,2,  17,1,  17,1,0,  KeepX),
                         c([ecUp, ecDown],                  8,2,  16,1,  16,1,0,  not KeepX)
                        ]);
    CheckXyMap('wcpBOL', p( 1,3,  10,2,   18,1,   18,1)); // after " "
    CheckXyMap('wcpBOL', p( 2,3,          19,1,   19,1)); // after "X"
    CheckXyMap('wcpBOL', p( 4,3,          21,1,   21,1)); // after "Z" EOL
    if AllowPastEOL then
    CheckXyMap('wcpBOL', p( 5,3,          22,1,   22,1)); // at EOL + 1

    CheckXyMap('wcpBOL', p( 1,4,           1,2,    1,2),
                        [c([ecDown],                  1,5,  11,2,   7,2,0),
                         c([ecDown,ecDown],           1,6,  21,2,  15,2,0),
                         c([ecDown,ecDown,ecDown],    1,7,   1,3,   1,3,0)
                        ]);
    CheckXyMap('wcpBOL', p( 2,4,           2,2,    2,2, 0)); // before tab
    if not SkipTab then
    CheckXyMap('wcpBOL', p( 3,4,           3,2,    2,2, 1)); // 1 inside tab
    CheckXyMap('wcpBOL', p( 5,4,           5,2,    3,2, 0)); // after tab
    CheckXyMap('wcpBOL', p( 9,4,           9,2,    5,2)); // after tab, before "C"
    CheckXyMap('wcpBOL', p(10,4,          10,2,    6,2), // after "C"
                        [c([ecDown],                        8,5,  18,2,   14,2,0, SkipTab),      // G#9H#|9
                         c([ecDown],                       10,5,  20,2,   14,2,2, not SkipTab),  // G#9H#|9
                         c([ecDown,ecDown],                10,6,  30,2,   18,2,0,  KeepX),       // #9#9X|Y
                         c([ecDown,ecDown],                 5,6,  25,2,   16,2,0,  (not KeepX) and SkipTab)    // #9#9X|Y
                        ]);
    CheckXyMap('wcpBOL', p( 1,5,  11,4,   11,2,    7,2)); // after " "
    CheckXyMap('wcpBOL', p( 2,5,          12,2,    8,2)); // after "D"
    CheckXyMap('wcpBOL', p( 8,5,          18,2,   14,2)); // after "H"
    if not SkipTab then
    CheckXyMap('wcpBOL', p( 9,5,          19,2,   14,2, 1)); // in #9
    if not SkipTab then
    CheckXyMap('wcpBOL', p(10,5,          20,2,   14,2, 2)); // in #9
    CheckXyMap('wcpBOL', p( 1,6,  11,5,   21,2,   15,2)); // after #9
    if not SkipTab then
    CheckXyMap('wcpBOL', p( 2,6,          22,2,   15,2, 1)); // in #9 / next line
    CheckXyMap('wcpBOL', p( 5,6,          25,2,   16,2)); // after 1st #9 / next line
    CheckXyMap('wcpBOL', p(10,6,          30,2,   18,2)); // after "x"
    CheckXyMap('wcpBOL', p(11,6,          31,2,   19,2), // after "y" EOL
                        [c([ecUp],                        8,5,  18,2,   14,2,0, SkipTab),      // G#9H#|9
                         c([ecUp],                       10,5,  20,2,   14,2,2, not SkipTab),  // G#9H#|9
                         c([ecUp,ecDown],                11,6,  31,2,   19,2,0,  KeepX)
                        ]);

    CheckXyMap('wcpBOL', p( 1,7,           1,3,    1,3));
    CheckXyMap('wcpBOL', p( 2,7,           2,3,    3,3));
    CheckXyMap('wcpBOL', p( 8,7,           8,3,   14,3)); // after "ö"
    CheckXyMap('wcpBOL', p( 1,8,   9,7,    9,3,   15,3)); // after " "
    CheckXyMap('wcpBOL', p( 2,8,          10,3,   17,3)); // after "Ä"
    CheckXyMap('wcpBOL', p( 1,9,   9,8,   17,3,   29,3)); // after " "

  end;


  CheckLineIndexMapping('LineMap 0',   0,   0, 2);
  CheckLineIndexMapping('LineMap 1',   1,   3, 5);
  CheckLineIndexMapping('LineMap 2',   2,   6, 8);
  CheckLineIndexMapping('LineMap 3',   3,   9, 9);


  SynEdit.ExecuteCommand(ecEditorBottom, '', nil);
  AssertEquals('ecEditorBottom', 4 ,SynEdit.CaretY);
  AssertEquals('ecEditorBottom', 10 ,SynEdit.CaretObj.ViewedLineCharPos.y);

  SetSynEditWidth(65);
  AddLines(0, 6000, 60, 'A');

  CheckLine('', 0, 'A_0_0');
  CheckLine('', 1, 'A_1_0');
  CheckLine('', 2, 'A_2_0');
  CheckLine('', 3, 'A_3_0');

  SetSynEditWidth(35);
  CheckLine('', 0, 'A_0_0');
  CheckLine('', 1, 'A_0_3');
  CheckLine('', 2, 'A_1_0');

// '		'

end;

procedure TTestWordWrapPlugin.TestWrapSplitJoin;
  procedure AddLineTestCount(AName: String; ALineIdx, ACount, ALen: Integer; AExpCount: Integer);
  begin
    AddLines(ALineIdx, ACount, ALen, 'A');
    AssertEquals(Format('%s : After ins %d Line(s) at %d  Len %d', [AName, ACount, ALineIdx, ALen]), AExpCount, TreeNodeCount);
  end;
  procedure AddLineTestCount(AName: String; ALineIdx, ALen: Integer; AExpCount: Integer);
  begin
    AddLineTestCount(AName, ALineIdx, 1, ALen, AExpCount);
  end;

  procedure ChangeLineTestCount(AName: String; ALineIdx, ALen: Integer; AExpCount: Integer);
  begin
    AddLines(ALineIdx, 1, ALen, 'A', False, True);
    AssertEquals(Format('%s : After ins %d Line(s) at %d  Len %d', [AName, 1, ALineIdx, ALen]), AExpCount, TreeNodeCount);
  end;
  procedure ChangeLineTestCount(AName: String; ALineIdx, ANodeIdx, ALen: Integer; AExpCount: Integer);
  var
    n: TSynEditLineMapPageHolder;
  begin
    n := TreeNodeHolder[ANodeIdx];
    if ALineIdx >= 0
    then AddLines(n.RealStartLine + ALineIdx, 1, ALen, 'A', False, True)
    else AddLines(n.RealEndLine + 1 + ALineIdx, 1, ALen, 'A', False, True);
    AssertEquals(Format('%s : After ins %d Line(s) at %d  Len %d', [AName, 1, ALineIdx, ALen]), AExpCount, TreeNodeCount);
  end;

  procedure DelLineTestCount(AName: String; ALineIdx: Integer; AExpCount: Integer);
  begin
    if ALineIdx < 0
    then SynEdit.Lines.Delete(SynEdit.Lines.Count + 1 + ALineIdx)
    else SynEdit.Lines.Delete(ALineIdx);
    AssertEquals(Format('%s : After DEL Line at %d  Len %d', [AName, ALineIdx]), AExpCount, TreeNodeCount);
  end;
  procedure DelLineTestCount(AName: String; ANodeIdx, ALineIdx: Integer; AExpCount: Integer);
  var
    n: TSynEditLineMapPageHolder;
  begin
    n := TreeNodeHolder[ANodeIdx];
    if ALineIdx < 0
    then SynEdit.Lines.Delete(n.RealEndLine + 1 + ALineIdx)
    else SynEdit.Lines.Delete(n.RealStartLine + ALineIdx);
    AssertEquals(Format('%s : After DEL Line at %d  Len %d', [AName, ALineIdx]), AExpCount, TreeNodeCount);
  end;

var
  t: TSynLineMapAVLTree;
  n1, n2: TSynEditLineMapPageHolder;
  i: Integer;
begin
  ReCreateEdit(10);
  SynEdit.Options := [];
  t := FWordWrap.FLineMapView.Tree;
debugln(' split %d  join %d  dist %d', [t.PageSplitSize, t.PageJoinSize, t.PageJoinDistance]);

  AddLineTestCount('new: split - 2', 0, t.PageSplitSize - 2, 18,    1);
  AddLineTestCount('insert start: split - 1', 0, 18,    1);
  AddLineTestCount('insert start: split - 0', 0, 18,    1);
  AddLineTestCount('insert start: split + 1', 0, 18,    2);
  AddLineTestCount('insert start: split + 2', 0, 18,    2);


  ReCreateEdit(10);
  SynEdit.Options := [];
  t := FWordWrap.FLineMapView.Tree;
  AddLineTestCount('new: split - 2', 0, t.PageSplitSize - 2, 18,    1);
  AddLineTestCount('insert end: split - 1', SynEdit.Lines.Count, 18,    1);
  AddLineTestCount('insert end: split - 0', SynEdit.Lines.Count, 18,    1);
  AddLineTestCount('insert end: split + 1', SynEdit.Lines.Count, 18,    2);
  AddLineTestCount('insert end: split + 2', SynEdit.Lines.Count, 18,    2);


  ReCreateEdit(10);
  SynEdit.Options := [];
  t := FWordWrap.FLineMapView.Tree;
  AddLineTestCount('new: split - 2', 0, t.PageSplitSize - 2, 18,    1);
  AddLineTestCount('insert @10: split - 1', 10, 18,    1);
  AddLineTestCount('insert @10: split - 0', 10, 18,    1);
  AddLineTestCount('insert @10: split + 1', 10, 18,    2);
  AddLineTestCount('insert @10: split + 2', 10, 18,    2);


  ReCreateEdit(10);
  SynEdit.Options := [];
  t := FWordWrap.FLineMapView.Tree;
  i := t.PageSplitSize - 2;
  AddLineTestCount('new: split - 2', 0,  i, 18,    1);
  AddLineTestCount('new: split - 2', i, 10,  1,    1);
  ChangeLineTestCount('update end: split - 1', i, 18,   1);   inc(i);
  ChangeLineTestCount('update end: split - 0', i, 18,   1);   inc(i);
  ChangeLineTestCount('update end: split + 1', i, 18,   2);   inc(i);
  ChangeLineTestCount('update end: split + 2', i, 18,   2);   inc(i);


  ReCreateEdit(10);
  SynEdit.Options := [];
  t := FWordWrap.FLineMapView.Tree;
  i := 9;
  AddLineTestCount('new: split - 2', 0,  t.PageSplitSize - 2, 18,    1);
  AddLineTestCount('new: split - 2', 0, 10,  1,    1);
  ChangeLineTestCount('update start: split - 1', i, 18,   1);   dec(i);
  ChangeLineTestCount('update start: split - 0', i, 18,   1);   dec(i);
  ChangeLineTestCount('update start: split + 1', i, 18,   2);   dec(i);
  ChangeLineTestCount('update start: split + 2', i, 18,   2);   dec(i);



  ///////////////////
  ReCreateEdit(10);
  SynEdit.Options := [];
  t := FWordWrap.FLineMapView.Tree;
  AddLineTestCount('new: split double', 0,  t.PageSplitSize + t.PageJoinSize + 2, 18,    2);


  n1 := TreeNodeHolder[0];
  while n1.RealCount > t.PageJoinSize + 1 do
    SynEdit.Lines.Delete(n1.RealStartLine + 2);
  AssertEquals('insert start: split + 2', 2, TreeNodeCount);

  n2 := TreeNodeHolder[1];
  while n2.RealCount > t.PageJoinSize + 1 do
    SynEdit.Lines.Delete(n2.RealStartLine + 2);
  AssertEquals('insert start: split + 2', 2, TreeNodeCount);

  SynEdit.Lines.Delete(1);
  SynEdit.Lines.Delete(SynEdit.Lines.Count - 2);

  AssertEquals('insert start: split + 2', 1, TreeNodeCount);





  ReCreateEdit(10);
  SynEdit.Options := [];
  t := FWordWrap.FLineMapView.Tree;
  AddLineTestCount('new: split double', 0,  t.PageSplitSize + t.PageJoinSize + 2, 18,    2);


  n1 := TreeNodeHolder[0];
  while n1.RealCount > t.PageJoinSize + 1 do
    ChangeLineTestCount('edit n1 start', 0,0,  1,   2);
  n2 := TreeNodeHolder[1];
  while n2.RealCount > t.PageJoinSize + 1 do
    ChangeLineTestCount('edit n2 end', -1,1,  1,   2);

  ChangeLineTestCount('edit n1 start',  0,0,  1,   2);
  ChangeLineTestCount('edit n2 end  ', -1,1,  1,   1);




  // do not join
  ReCreateEdit(10);
  SynEdit.Options := [];
  t := FWordWrap.FLineMapView.Tree;
  AddLineTestCount('new: split * 2 - 2', 0,  t.PageSplitSize * 2 - 2, 18,    2);


  n1 := TreeNodeHolder[0];
  while n1.RealCount > t.PageJoinSize + 1 do
    ChangeLineTestCount('edit n1 end', -1,0,  1,   2);
  n2 := TreeNodeHolder[1];
  while n2.RealCount > t.PageJoinSize + 1 do
    ChangeLineTestCount('edit n2 start', 0,1,  1,   2);

  ChangeLineTestCount('edit n1 start', -1,0,  1,   2);
  ChangeLineTestCount('edit n2 end  ',  0,1,  1,   2);
  ChangeLineTestCount('edit n1 start', -1,0,  1,   2);
  ChangeLineTestCount('edit n2 end  ',  0,1,  1,   2);
  ChangeLineTestCount('edit n1 start', -1,0,  1,   2);
  ChangeLineTestCount('edit n2 end  ',  0,1,  1,   2);



end;

procedure TTestWordWrapPlugin.TestEditorEdit;
begin
  SynEdit.Options := [];
  SynEdit.TabWidth := 4;

  SetLines([
    'abc def ' + 'ABC DEFG ' + 'XYZ',
    '',
    //'A'  #9'B'  #9'C ' + 'DEF G'#9'H'  #9 + ''   #9   #9'xy',
    'A'#9'B'#9'C ' + 'DEF G'#9'H'#9 + #9#9'xy',
    '',
    'äää ööö ' + 'ÄÄÄ ÖÖÖ ' + 'ÜÜÜ',
    '',
    '999'
  ]);
  SetSynEditWidth(10);

  SetSynEditWidth(10);
  CheckLines('', 0, [
    'abc def ',
    'ABC DEFG ',
    'XYZ',
    '',
    'A'#9'B'#9'C ',
    'DEF G'#9'H'#9,
    #9#9'xy',
    '',
    'äää ööö ',
    'ÄÄÄ ÖÖÖ ',
    'ÜÜÜ',
    '',
    '999'
  ], True);

  SynEdit.BeginUpdate;
  SynEdit.TestTypeText(1,7, '4 ');
  SynEdit.TestTypeText(1,3, '2 ');
  SynEdit.TestTypeText(1,5, '3 ');
  SynEdit.TestTypeText(1,1, '1 ');
  SynEdit.EndUpdate;

  CheckLines('', 0, [
    '1 abc def ',
    'ABC DEFG ',
    'XYZ',
    '',
    '2 A'#9'B'#9'C ',
    'DEF G'#9'H'#9,
    #9#9'xy',
    '',
    '3 äää ööö ',
    'ÄÄÄ ÖÖÖ ',
    'ÜÜÜ',
    '',
    '4 999'
  ], True);
end;

initialization

  RegisterTest(TTestWordWrap);
  RegisterTest(TTestWordWrapPlugin);
end.

