unit TestSyncroEdit;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, Math, testregistry, LCLType, LazLoggerBase, Forms, TestBase,
  SynEdit, SynPluginSyncroEdit, SynEditKeyCmds, SynPluginTemplateEdit,
  SynPluginSyncronizedEditBase, SynEditTypes;

type

  PSynPluginCustomSyncroEdit = ^TSynPluginCustomSyncroEdit;

  TTestSynPluginSyncroEdit = class(TSynPluginSyncroEdit)
  public
    property Cells;
    property CurrentCell;
  end;
  TTestSynPluginTemplateEdit = class(TSynPluginTemplateEdit)
  public
    property Cells;
    property CurrentCell;
  end;

  TCellData = record
    PStart, PEnd: TPoint;
    Group: Integer;
  end;
  TCellsData = array of TCellData;

  function c(x1,y1, x2,y2: Integer; g: integer = -99): TCellData;
  function cs(const AData: array of TCellData): TCellsData;

type

  { TTestSyncroEdit }

  TTestSyncroEdit = class(TTestBase)
  protected
    FCurModule: PSynPluginCustomSyncroEdit;
    FSyncroModule: TTestSynPluginSyncroEdit;
    FTemplateModule: TTestSynPluginTemplateEdit;

    FDoUndo: Integer; // repeated undo/redo cycles
    FMaxUndoSteps: Integer; // steps of undo/redo in each cycle

    FExpectUndoneList: array of record
      SyncActive: Boolean;
      FullText: String;
      Caret: TPoint;
      CellData: TCellsData;
      CurrentCell: Integer;
      ExpCellData: TCellsData;
      ExpCurrentCell: Integer;
    end;

    FName: String;
    procedure Dump(hsh: TSynPluginSyncroEditWordsHash);
    procedure ReCreateEdit; reintroduce;
    procedure StartSyncroMode;

    function CurrentCell: Integer;
    function CopyCells: TCellsData;
    procedure CheckCells(AName: String; AExp: array of TCellData);
    procedure SetCarretAndCell(X, Y, C: Integer);


    procedure TestCmdAt(AName: String; X, Y: Integer; Cmd: TSynEditorCommand;
                         Exp: Array of String; SyncRunning: Boolean = True;
                         ExpCurCell: Integer = -99; ExpCells: TCellsData = nil);
    procedure TestKeyAt(AName: String; X, Y: Integer; Key: Word; Shift: TShiftState;
                         Exp: Array of String; SyncRunning: Boolean = True;
                         ExpCurCell: Integer = -99; ExpCells: TCellsData = nil);
    procedure TestKeyAtSel(AName: String; X, Y, X2, Y2: Integer; Key: Word; Shift: TShiftState;
                         Exp: Array of String; SyncRunning: Boolean = True;
                         ExpCurCell: Integer = -99; ExpCells: TCellsData = nil);

    procedure TestUndoRedo(const AName: String; const Exp: Array of String; SyncRunning: Boolean);

  published
    procedure WordsHash;
    (* SyncroEdit
       must NOT add any undo entries (class TSynPluginSyncronizedEditUndoCurrentCell)
       TODO: add checks...
       Until then, run test with breakpoint in TSynPluginSyncronizedEditUndoCurrentCell.create
    *)
    procedure SyncroEdit;
    procedure SyncroTemplateEdit;
  end;

implementation

function c(x1, y1, x2, y2: Integer; g: integer): TCellData;
begin
  Result.PStart := Point(X1, Y1);
  Result.PEnd := Point(X2, Y2);
  Result.Group := g;
end;

function cs(const AData: array of TCellData): TCellsData;
var
  i: Integer;
begin
  SetLength(Result, Length(AData));
  for i := 0 to Length(AData) - 1 do
    Result[i] := AData[i];
end;

procedure TTestSyncroEdit.Dump(hsh: TSynPluginSyncroEditWordsHash);
var
  he: TSynPluginSyncroEditWordsHashEntry;
  i, j: Integer;
begin
  debugln(['Dump ', hsh.HashSize]);
  for i := 0 to hsh.HashSize - 1 do begin
    he := hsh.HashEntry[i, 0];
    if he.Count > 0 then begin
      debugln(['hash for ', i,', ', 0, '   Cnt=', he.Count, ' hsh=', he.Hash, ' nxt=', he.Next, ' y=', he.LineIdx, ' x=', he.BytePos, ' ln=', he.Len]);
      j := 1;
      he := hsh.HashEntry[i, j];
      while he.Count > 0 do begin
        debugln(['  hash for ', i,', ', j, ' Cnt=', he.Count, ' hsh=', he.Hash, ' nxt=', he.Next, ' y=', he.LineIdx, ' x=', he.BytePos, ' ln=', he.Len]);
        inc(j);
        he := hsh.HashEntry[i, j];
      end;
    end;
  end;
end;

procedure TTestSyncroEdit.ReCreateEdit;
begin
  inherited;
if FCurModule = @FSyncroModule then
  FSyncroModule := TTestSynPluginSyncroEdit.Create(SynEdit) //;
else
  FTemplateModule := TTestSynPluginTemplateEdit.Create(SynEdit);
  FExpectUndoneList := nil;
end;

procedure TTestSyncroEdit.StartSyncroMode;
var
  i: Integer;
begin
  //DoKeyPress(VK_J, [ssCtrl]);
  SynEdit.CommandProcessor(ecSynPSyncroEdStart, '', nil);
  i := 5;
  while (i > 0) and (not FSyncroModule.Active) do begin
    dec(i);
    Application.ProcessMessages;
  end;
  AssertTrue('SyncroMode started', FSyncroModule.Active );
  Application.ProcessMessages;
end;

function TTestSyncroEdit.CurrentCell: Integer;
begin
  if not FCurModule^.Active then
    Result := -1
  else
  if FCurModule^ = FSyncroModule then
    Result := FSyncroModule.CurrentCell
  else
    Result := FTemplateModule.CurrentCell;
end;

function TTestSyncroEdit.CopyCells: TCellsData;
var
  cells: TSynPluginSyncronizedEditList;
  i, l: Integer;
begin
  Result := nil;
  if not FCurModule^.Active then
    exit;
  if FCurModule^ = FSyncroModule then
    cells := FSyncroModule.Cells
  else
    cells := FTemplateModule.Cells;

  l := cells.Count;
  if cells[l-1].Group = -2 then // ignore final caret
    dec(l);

  SetLength(Result, l);
  for i := 0 to l-1 do begin
    Result[i].PStart := cells[i].LogStart;
    Result[i].PEnd := cells[i].LogEnd;
    Result[i].Group := cells[i].Group;
  end;
end;

procedure TTestSyncroEdit.CheckCells(AName: String; AExp: array of TCellData);
var
  cells: TSynPluginSyncronizedEditList;
  i, l: Integer;
begin
  if FCurModule^ = FSyncroModule then
    cells := FSyncroModule.Cells
  else
    cells := FTemplateModule.Cells;

  l := cells.Count;
  if cells[l-1].Group = -2 then // ignore final caret
    dec(l);

  AssertEquals(AName+' Len', Length(AExp), l);
  for i := 0 to l-1 do begin
    AssertEquals(AName+' Cell:'+inttostr(i)+' S.x', AExp[i].PStart.x, cells[i].LogStart.x);
    AssertEquals(AName+' Cell:'+inttostr(i)+' S.y', AExp[i].PStart.y, cells[i].LogStart.y);
    AssertEquals(AName+' Cell:'+inttostr(i)+' E.x', AExp[i].PEnd.x, cells[i].LogEnd.x);
    AssertEquals(AName+' Cell:'+inttostr(i)+' E.y', AExp[i].PEnd.y, cells[i].LogEnd.y);
    if AExp[i].Group <> -99 then
      AssertEquals(AName+' Cell:'+inttostr(i)+' Grp', AExp[i].Group, cells[i].Group);
  end;
end;

procedure TTestSyncroEdit.SetCarretAndCell(X, Y, C: Integer);
begin
  SetCaret(x, y);
  if FCurModule^ = FSyncroModule then
    FSyncroModule.CurrentCell := c
  else
    FTemplateModule.CurrentCell := c;
end;

procedure TTestSyncroEdit.TestCmdAt(AName: String; X, Y: Integer;
  Cmd: TSynEditorCommand; Exp: array of String; SyncRunning: Boolean;
  ExpCurCell: Integer; ExpCells: TCellsData);
var
  t: String;
  l: Integer;
begin
  t := SynEdit.Text;
  SetCaret(x, y);

  l := Length(FExpectUndoneList);
  SetLength(FExpectUndoneList, l+1);
  FExpectUndoneList[l].SyncActive := FCurModule^.Active;
  FExpectUndoneList[l].FullText := t;
  FExpectUndoneList[l].Caret := SynEdit.CaretObj.LineBytePos;
  FExpectUndoneList[l].CellData := CopyCells;
  FExpectUndoneList[l].CurrentCell := CurrentCell;
  FExpectUndoneList[l].ExpCellData := ExpCells;
  FExpectUndoneList[l].ExpCurrentCell := ExpCurCell;

  SynEdit.CommandProcessor(Cmd, '', nil);
  TestIsFullText(Format('Cmd at (%d,%d) %s / %s', [X, Y, FName, AName]), Exp);
  AssertEquals(Format('Cmd at (%d,%d) %s / %s - Sync Running', [X, Y, FName, AName]), SyncRunning, FCurModule^.Active );

  if ExpCurCell <> -99 then
    AssertEquals(Format('Cmd at (%d,%d) %s / %s CurrentCell', [X, Y, FName, AName]), ExpCurCell, CurrentCell);
  if ExpCells <> nil then
    CheckCells(Format('Cmd at (%d,%d) %s / %s', [X, Y, FName, AName]), ExpCells);

  TestUndoRedo(Format('Cmd at (%d,%d) %s / %s ', [X, Y, FName, AName]), Exp, SyncRunning);
end;

procedure TTestSyncroEdit.TestKeyAt(AName: String; X, Y: Integer; Key: Word;
  Shift: TShiftState; Exp: array of String; SyncRunning: Boolean;
  ExpCurCell: Integer; ExpCells: TCellsData);
var
  t: String;
  i, UCnt, j, l: Integer;
begin
  t := SynEdit.TestFullText;
  SetCaret(x, y);

  l := Length(FExpectUndoneList);
  SetLength(FExpectUndoneList, l+1);
  FExpectUndoneList[l].SyncActive := FCurModule^.Active;
  FExpectUndoneList[l].FullText := t;
  FExpectUndoneList[l].Caret := SynEdit.CaretObj.LineBytePos;
  FExpectUndoneList[l].CellData := CopyCells;
  FExpectUndoneList[l].CurrentCell := CurrentCell;
  FExpectUndoneList[l].ExpCellData := ExpCells;
  FExpectUndoneList[l].ExpCurrentCell := ExpCurCell;

debugln(['!!!! key']);
  DoKeyPress(Key, Shift);
  TestIsFullText(Format('KeyPress at (%d,%d) %s / %s', [X, Y, FName, AName]), Exp);
  AssertEquals(Format('KeyPress at (%d,%d) %s / %s - Sync Running', [X, Y, FName, AName]), SyncRunning, FCurModule^.Active );

  if ExpCurCell <> -99 then
    AssertEquals(Format('KeyPress at (%d,%d) %s / %s CurrentCell', [X, Y, FName, AName]), ExpCurCell, CurrentCell);
  if ExpCells <> nil then
    CheckCells(Format('KeyPress at (%d,%d) %s / %s', [X, Y, FName, AName]), ExpCells);

  TestUndoRedo(Format('KeyPress at (%d,%d) %s / %s ', [X, Y, FName, AName]), Exp, SyncRunning);
debugln(['!!!! end']);
end;

procedure TTestSyncroEdit.TestKeyAtSel(AName: String; X, Y, X2, Y2: Integer;
  Key: Word; Shift: TShiftState; Exp: array of String; SyncRunning: Boolean;
  ExpCurCell: Integer; ExpCells: TCellsData);
var
  t: String;
  l: Integer;
begin
  t := SynEdit.Text;
  SetCaretAndSel(x, y, X2, Y2);

  l := Length(FExpectUndoneList);
  SetLength(FExpectUndoneList, l+1);
  FExpectUndoneList[l].SyncActive := FCurModule^.Active;
  FExpectUndoneList[l].FullText := t;
  FExpectUndoneList[l].Caret := SynEdit.CaretObj.LineBytePos;
  FExpectUndoneList[l].CellData := CopyCells;
  FExpectUndoneList[l].CurrentCell := CurrentCell;
  FExpectUndoneList[l].ExpCellData := ExpCells;
  FExpectUndoneList[l].ExpCurrentCell := ExpCurCell;

  DoKeyPress(Key, Shift);
  TestIsFullText(Format('KeyPress+Sel at (%d,%d) %s / %s', [X, Y, FName, AName]), Exp);
  AssertEquals(Format('KeyPress+Sel at (%d,%d) %s / %s - Sync Running', [X, Y, FName, AName]), SyncRunning, FCurModule^.Active );

  if ExpCurCell <> -99 then
    AssertEquals(Format('KeyPress+Sel at (%d,%d) %s / %s CurrentCell', [X, Y, FName, AName]), ExpCurCell, CurrentCell);
  if ExpCells <> nil then
    CheckCells(Format('KeyPress+Sel at (%d,%d) %s / %s', [X, Y, FName, AName]), ExpCells);

  TestUndoRedo(Format('KeyPress+Sel at (%d,%d) %s / %s ', [X, Y, FName, AName]), Exp, SyncRunning);
end;

procedure TTestSyncroEdit.TestUndoRedo(const AName: String; const Exp: array of String; SyncRunning: Boolean);
var
  i, j: Integer;
  UCnt, l: Integer;
begin
  l := Length(FExpectUndoneList);
  UCnt := min(l, Max(1, FMaxUndoSteps));

  for i := 1 to FDoUndo do begin
    for j := 1 to UCnt do begin
debugln(['!!!! undo',j]);
      SynEdit.Undo;
      if not FExpectUndoneList[l-j].SyncActive then
        SyncRunning := False; // undone gone before template activation, template will be off, even in redo

      TestIsFullText(Format('%s UNDONE %d', [AName, j]), FExpectUndoneList[l-j].FullText);
      TestIsCaret(Format('%s UNDONE %d', [AName, j]), FExpectUndoneList[l-j].Caret.x, FExpectUndoneList[l-j].Caret.y);
      if SyncRunning then begin // no cells, if expected to have gone inactive
        CheckCells(Format('%s UNDONE %d Cells', [AName, j]), FExpectUndoneList[l-j].CellData);
        AssertEquals(Format('%s UNDONE %d CurCell', [AName, j]), FExpectUndoneList[l-j].CurrentCell, CurrentCell);
      end;
    end;

    for j := UCnt downto 1 do begin
debugln(['!!!! redo',j]);
      SynEdit.Redo;
      if j > 1 then begin
        TestIsFullText(Format('%s UNDONE %d', [AName, j]), FExpectUndoneList[l+1-j].FullText);
        // caret may on other location / stored loc is set by next test
        if SyncRunning then
          CheckCells(Format('%s UNDONE %d Cells', [AName, j]), FExpectUndoneList[l+1-j].CellData);
      end;

      if SyncRunning then begin
        if FExpectUndoneList[l-j].ExpCurrentCell <> -99 then
          AssertEquals(Format('%s UNDONE %d CurrentCell', [AName, j]), FExpectUndoneList[l-j].ExpCurrentCell, CurrentCell);
        if FExpectUndoneList[l-j].ExpCellData <> nil then
          CheckCells(Format('%s UNDONE %d Cells', [AName, j]), FExpectUndoneList[l-j].ExpCellData);
      end;

    end;
    TestIsFullText(Format('%s REDONE', [AName]), Exp);

    AssertEquals(Format('%s - REDONE Sync Running', [AName]), SyncRunning, FCurModule^.Active );
  end;
end;

procedure TTestSyncroEdit.WordsHash;
var
  hsh: TSynPluginSyncroEditWordsHash;

  function Check(Msg, Wrd: String; Cnt: Integer): TSynPluginSyncroEditWordsHashEntry;
  begin
    Result:= hsh.GetWord(@Wrd[1], length(Wrd));
    AssertEquals(Msg + ' ' + Wrd + ' Cnt', Cnt, Result.Count);
    if Cnt > 0 then
      AssertEquals(Msg + ' ' + Wrd + ' Len', length(Wrd), Result.Len);
  end;
  procedure Add(Wrd: String; Y, X: Integer);
  begin
    hsh.AddWord(y, x, length(Wrd), @Wrd[1]);
    //Dump(hsh);
  end;
  procedure Del(Wrd: String);
  begin
    hsh.RemoveWord(length(Wrd), @Wrd[1]);
    //Dump(hsh);
  end;

var
  lwl: TSynPluginSyncroEditLowerLineCache;
  s: String;
  i: Integer;
begin
  lwl := TSynPluginSyncroEditLowerLineCache.Create;
  lwl.Lines := SynEdit.ViewedTextBuffer;
  try
    hsh := TSynPluginSyncroEditWordsHash.Create;
    hsh.LowerLines := lwl;

    SynEdit.Lines.Add('Test abc');
    SynEdit.Lines.Add('atdesktop before2 252'); // supposed to have the same hash on a 4096 table
    SynEdit.Lines.Add('mk_equal ecpageup'); // same big hash

    Add('test', 0, 1);  Check('one word', 'test', 1);
    Add('test', 0, 1);  Check('one word twice', 'test', 2);
    Del('test');        Check('one word one down again', 'test', 1);
    Del('test');        Check('one word gone', 'test', 0);

    s:= 'atdesktop before2 252';
    AssertEquals('clash for atdesktop before2', hsh.GetWordModHash(@s[1], 9), hsh.GetWordModHash(@s[11], 7));
    AssertEquals('clash for atdesktop 252',     hsh.GetWordModHash(@s[1], 9), hsh.GetWordModHash(@s[19], 3));
    (* repeat test, but with double entry*)
    // add word with bad pointer, so we can create a clash

    Add('atdesktop', 1, 1); s:='2W  1'; Check(s, 'atdesktop', 1); Check(s, 'before2', 0); Check(s, '252', 0);
    Add('before2', 1, 11);  s:='2W  2'; Check(s, 'atdesktop', 1); Check(s, 'before2', 1); Check(s, '252', 0);
    Del('atdesktop');       s:='2W  3'; Check(s, 'atdesktop', 0); Check(s, 'before2', 1); Check(s, '252', 0);
    Add('atdesktop', 1, 1); s:='2W  4'; Check(s, 'atdesktop', 1); Check(s, 'before2', 1); Check(s, '252', 0);
    Add('atdesktop', 1, 1); s:='2W  5'; Check(s, 'atdesktop', 2); Check(s, 'before2', 1); Check(s, '252', 0);
    Del('atdesktop');       s:='2W  6'; Check(s, 'atdesktop', 1); Check(s, 'before2', 1); Check(s, '252', 0);
    Add('before2', 1, 11);  s:='2W  7'; Check(s, 'atdesktop', 1); Check(s, 'before2', 2); Check(s, '252', 0);
    Del('before2');         s:='2W  8'; Check(s, 'atdesktop', 1); Check(s, 'before2', 1); Check(s, '252', 0);
    Add('before2', 1, 11);  s:='2W  9'; Check(s, 'atdesktop', 1); Check(s, 'before2', 2); Check(s, '252', 0);
    Add('atdesktop', 1, 1); s:='2W 10'; Check(s, 'atdesktop', 2); Check(s, 'before2', 2); Check(s, '252', 0);
    Add('atdesktop', 1, 1); s:='2W 11'; Check(s, 'atdesktop', 3); Check(s, 'before2', 2); Check(s, '252', 0);
    Add('252', 1, 19);      s:='2W 12'; Check(s, 'atdesktop', 3); Check(s, 'before2', 2); Check(s, '252', 1);
    Del('before2');         s:='2W 13'; Check(s, 'atdesktop', 3); Check(s, 'before2', 1); Check(s, '252', 1);
    Del('before2');         s:='2W 14'; Check(s, 'atdesktop', 3); Check(s, 'before2', 0); Check(s, '252', 1);
    Del('before2');         s:='2W 15'; Check(s, 'atdesktop', 3); Check(s, 'before2', 0); Check(s, '252', 1); // none to del
    Del('252');             s:='2W 16'; Check(s, 'atdesktop', 3); Check(s, 'before2', 0); Check(s, '252', 0);
    Del('252');             s:='2W 17'; Check(s, 'atdesktop', 3); Check(s, 'before2', 0); Check(s, '252', 0); // none to del
    Del('atdesktop');       s:='2W 18'; Check(s, 'atdesktop', 2); Check(s, 'before2', 0); Check(s, '252', 0);
    hsh.Clear;              s:='2W 19'; Check(s, 'atdesktop', 0); Check(s, 'before2', 0); Check(s, '252', 0);

    Add('mk_equal', 2, 1);  s:='3W  1'; Check(s, 'mk_equal', 1); Check(s, 'ecpageup', 0);
    Add('ecpageup', 2, 10); s:='3W  2';
    AssertEquals('same hash', Check(s, 'mk_equal', 1).Hash, Check(s, 'ecpageup', 1).Hash);

    Add('ecpageup', 2, 1);  s:='3W  3'; Check(s, 'mk_equal', 1); Check(s, 'ecpageup', 2);
    Add('mk_equal', 2, 1);  s:='3W  4'; Check(s, 'mk_equal', 2); Check(s, 'ecpageup', 2);
    Del('mk_equal');        s:='3W  5'; Check(s, 'mk_equal', 1); Check(s, 'ecpageup', 2);
    Del('mk_equal');        s:='3W  6'; Check(s, 'mk_equal', 0); Check(s, 'ecpageup', 2);
    Del('mk_equal');        s:='3W  7'; Check(s, 'mk_equal', 0); Check(s, 'ecpageup', 2);
    hsh.Clear;

    // resize test
    for i := 0 to 5000 do begin
  //if i = 2200 then  Dump(hsh);
      SynEdit.Lines.Add(IntToStr(i));
      Add(inttostr(i), 3+i, 1);
      if i mod 7 = 0 then
        Add(inttostr(i), 3+i, 1);
    end;
    for i := 0 to 5000 do
      if i mod 7 = 0 then
        Check(inttostr(i), inttostr(i), 2)
      else
        Check(inttostr(i), inttostr(i), 1);

  finally
    hsh.free;
    lwl.Free;
  end;
end;

procedure TTestSyncroEdit.SyncroEdit;

  procedure SetTestText(TextIdx: Integer = 0);
  begin
    case TextIdx of
      0: SetLines(['abc foo',     // 1
                   'foo abc xy',  // 2
                   '  abc foo',   // 3
                   '',            // 4
                   'foo'          // 5
                  ]);
      1: SetLines(['abc foo',     // 1
                   'foo abc xy',  // 2
                   '  foo foo abc',//4
                   '',            // 5
                   'foo'          // 6
                  ]);
    end;
  end;
  procedure InitTestText(X1, Y1, X2, Y2: Integer; TextIdx: Integer = 0);
  begin
    ReCreateEdit;
    SynEdit.Options := SynEdit.Options - [eoGroupUndo, eoSmartTabDelete, eoSmartTabs] + [eoAutoIndent];
    SetTestText(TextIdx);
    SetCaretAndSel(X1, Y1, X2, Y2);
    StartSyncroMode;
  end;

  procedure TestSyncro;
  begin
    (* Edit at start/end of cell (insert/delete [backspace/del] )
       Edit cell, delete all, type again / Cut,Paste / newline
       Edit selection at start/end/complete of cell (delete/replace)
       Edit Cells, with 2 synced cells on same line

       Insert, delete linebreak in cell (with/without auto-indent)
         - includes some trim space
         - include 2 cells on one line

       Edit just before/after cell, while caret in cell (backspace at start / del at end)
         - same with emptied cell
       Edit just before/after cell, while caret in cell (selection outside cell / replace, delete)
         - part in/ part out of cell

       External Edit outside cell
TODO   External Edit inside current/other cell (or while caret outside any cell)

TODO   Lines[x] := ''; Lines.Add(); should deactivate the syncroMode

       TODO: more trim-space
       TODO: Delete word / last word => restrain to cell
       TODO: Change trim spaces, so it does keep spaces after undo
             see note in tests
    *)


    {%region Edit at start/end of cell (insert/delete [backspace/del] ) }
      {%region 'Insert/KeyPress inside cell' }
        Fname := 'Insert/KeyPress inside cell';

    // Tests that add only one undo-able step / no need to run again
    if FMaxUndoSteps<=1 then begin
        InitTestText(1,2, 1,4);
        TestKeyAt('Insert at Cell FOO (1 of 2), pos=1', 1, 2, VK_M, [],
          ['abc foo',
           'mfoo abc xy',
           '  abc mfoo',
           '',
           'foo',
           '']);

        InitTestText(1,2, 1,4);
        TestKeyAt('insert at cell FOO (1 of 2), pos=end', 4, 2, VK_M, [],
          ['abc foo',
           'foom abc xy',
           '  abc foom',
           '',
           'foo',
           '']);

        InitTestText(1,2, 1,4);
        TestKeyAt('insert at cell FOO (2 of 2), pos=1', 7, 3, VK_M, [],
          ['abc foo',
           'mfoo abc xy',
           '  abc mfoo',
           '',
           'foo',
           '']);

        InitTestText(1,2, 1,4);
        TestKeyAt('insert at cell FOO (2 of 2), pos=end', 10, 3, VK_M, [],
          ['abc foo',
           'foom abc xy',
           '  abc foom',
           '',
           'foo',
           '']);

        InitTestText(1,1, 1,4);
        TestKeyAt('insert at cell FOO (2 of 3), pos=1', 1, 2, VK_M, [],
          ['abc mfoo',
           'mfoo abc xy',
           '  abc mfoo',
           '',
           'foo',
           '']);
    end; // Tests that add only one undo-able step / no need to run again



        InitTestText(1,1, 1,4);
        TestKeyAt('insert at cell FOO (2 of 3), pos=end', 4, 2, VK_M, [],
          ['abc foom',
           'foom abc xy',
           '  abc foom',
           '',
           'foo',
           '']);
        TestKeyAt('insert(continue) at cell FOO (2 of 3), pos=new-end', 5, 2, VK_X, [],
          ['abc foomx',
           'foomx abc xy',
           '  abc foomx',
           '',
           'foo',
           '']);
        TestKeyAt('insert(continue) at cell FOO (2 of 3), pos=1', 1, 2, VK_N, [],
          ['abc nfoomx',
           'nfoomx abc xy',
           '  abc nfoomx',
           '',
           'foo',
           '']);
        TestKeyAt('insert(continue) at cell ABC(was FOO) (2 of 3), pos=1', 8, 2, VK_D, [],
          ['dabc nfoomx',
           'nfoomx dabc xy',
           '  dabc nfoomx',
           '',
           'foo',
           '']);
        TestKeyAt('insert(continue) at cell ABC(was FOO) (2 of 3), pos=end', 12, 2, VK_E, [],
          ['dabce nfoomx',
           'nfoomx dabce xy',
           '  dabce nfoomx',
           '',
           'foo',
           '']);
        TestKeyAt('insert(continue) at cell ABC(was FOO) (1 of 3), pos=mid', 3, 1, VK_F, [],
          ['dafbce nfoomx',
           'nfoomx dafbce xy',
           '  dafbce nfoomx',
           '',
           'foo',
           '']);
      {%endregion}
      {%region 'Delete/KeyPress inside cell' }
        Fname := 'Delete/KeyPress inside cell';

    // Tests that add only one undo-able step / no need to run again
    if FMaxUndoSteps<=1 then begin
        InitTestText(1,2, 1,4);
        TestKeyAt('Delete at Cell FOO (1 of 2), pos=1', 1, 2, VK_DELETE, [],
          ['abc foo',
           'oo abc xy',
           '  abc oo',
           '',
           'foo',
           '']);

        InitTestText(1,2, 1,4);
        TestKeyAt('Backspace at Cell FOO (1 of 2), pos=1', 2, 2, VK_BACK, [],
          ['abc foo',
           'oo abc xy',
           '  abc oo',
           '',
           'foo',
           '']);

        InitTestText(1,2, 1,4);
        TestKeyAt('Delete at cell FOO (1 of 2), pos=end', 3, 2, VK_DELETE, [],
          ['abc foo',
           'fo abc xy',
           '  abc fo',
           '',
           'foo',
           '']);

        InitTestText(1,2, 1,4);
        TestKeyAt('Backspace at cell FOO (1 of 2), pos=end', 4, 2, VK_BACK, [],
          ['abc foo',
           'fo abc xy',
           '  abc fo',
           '',
           'foo',
           '']);


        InitTestText(1,1, 1,4);
        TestKeyAt('Delete at Cell FOO (3 of 3), pos=1', 7, 3, VK_DELETE, [],
          ['abc oo',
           'oo abc xy',
           '  abc oo',
           '',
           'foo',
           '']);
    end; // Tests that add only one undo-able step / no need to run again

        InitTestText(1,1, 1,4);
        TestKeyAt('Backspace at cell FOO (3 of 3), pos=end', 10, 3, VK_BACK, [],
          ['abc fo',
           'fo abc xy',
           '  abc fo',
           '',
           'foo',
           '']);
        TestKeyAt('Delete(continue) at cell FOO (1 of 3), pos=end', 5, 1, VK_DELETE, [],
          ['abc o',
           'o abc xy',
           '  abc o',
           '',
           'foo',
           '']);
        TestKeyAt('Delete(continue) at cell ABC(was FOO) (2 of 3), pos=1', 3, 2, VK_DELETE, [],
          ['bc o',
           'o bc xy',
           '  bc o',
           '',
           'foo',
           '']);
        TestKeyAt('Backspace(continue) at cell ABC(was FOO) (2 of 3), pos=end', 5, 2, VK_BACK, [],
          ['b o',
           'o b xy',
           '  b o',
           '',
           'foo',
           '']);
      {%endregion}
    {%endregion}

    {%region Edit cell, delete all, type again }
      Fname := 'DeleteAll-Retype/KeyPress inside cell';

      InitTestText(1,2, 1,4);
      TestKeyAt('Delete at Cell FOO (1 of 2), pos=1', 1, 2, VK_DELETE, [],
        ['abc foo',
         'oo abc xy',
         '  abc oo',
         '',
         'foo',
         '']);
      TestKeyAt('Delete at Cell OO (1 of 2), pos=1', 1, 2, VK_DELETE, [],
        ['abc foo',
         'o abc xy',
         '  abc o',
         '',
         'foo',
         '']);
      TestKeyAt('Delete at Cell O (1 of 2), pos=1', 1, 2, VK_DELETE, [],
        ['abc foo',
         ' abc xy',
         '  abc ',
         '',
         'foo',
         '']);
      TestKeyAt('Retype(del) at Cell FOO (1 of 2), pos=1', 1, 2, VK_X, [],
        ['abc foo',
         'x abc xy',
         '  abc x',
         '',
         'foo',
         '']);
      TestKeyAt('Delete at Cell X (1 of 2), pos=1', 1, 2, VK_DELETE, [],
        ['abc foo',
         ' abc xy',
         '  abc ',
         '',
         'foo',
         '']);
if (FDoUndo = 0) then begin // todo: fails with undo
(* loosing trailing space on undo
   This is due to "FTrimmedLinesView.ForceTrim;" in SynEdit.Undo
   Which in turn is there, because the trimming in undo, must be done while the UndoList is still locked....
*)
      TestKeyAt('Retype / Insert new line', 1, 2, VK_RETURN, [],
        ['abc foo',
         '',' abc xy',
         '  abc ','',
         '',
         'foo',
         '']);
end;

      InitTestText(1,2, 1,4);
      TestCmdAt('DeleteLastWord at Cell FOO (1 of 2), pos=end', 4, 2, ecDeleteLastWord,
        ['abc foo',
         ' abc xy',
         '  abc ',
         '',
         'foo',
         '']);
      TestKeyAt('Retype(bck-sp) at Cell FOO (1 of 2)', 1, 2, VK_X, [],
        ['abc foo',
         'x abc xy',
         '  abc x',
         '',
         'foo',
         '']);

      InitTestText(1,2, 1,4);
      TestCmdAt('DeleteLastWord at Cell FOO (1 of 2), pos=end', 4, 2, ecDeleteLastWord,
        ['abc foo',
         ' abc xy',
         '  abc ',
         '',
         'foo',
         '']);
      TestKeyAt('Retype(bck-sp) at other Cell FOO (2 of 2)', 7, 3, VK_X, [],
        ['abc foo',
         'x abc xy',
         '  abc x',
         '',
         'foo',
         '']);

      InitTestText(1,2, 1,4);
      TestKeyAtSel('Cut at Cell FOO (1 of 2), pos=1', 4,2, 1,2, VK_X, [ssCtrl],
        ['abc foo',
         ' abc xy',
         '  abc ',
         '',
         'foo',
         '']);
      TestKeyAt('paste at Cell FOO (1 of 2), pos=1', 1, 2, VK_V, [ssCtrl],
        ['abc foo',
         'foo abc xy',
         '  abc foo',
         '',
         'foo',
         '']);
    {%endregion}

    {%region Edit selection at start/end/complete of cell (delete/replace) }
      Fname := 'Delete/Selection';

    // Tests that add only one undo-able step / no need to run again
    if FMaxUndoSteps<=1 then begin
      InitTestText(1,2, 1,4);
      TestKeyAtSel('at start of cell', 3,2, 1,2, VK_DELETE, [],
        ['abc foo',
         'o abc xy',
         '  abc o',
         '',
         'foo',
         '']);

      InitTestText(1,2, 1,4);
      TestKeyAtSel('at end of cell', 2,2, 4,2, VK_DELETE, [],
        ['abc foo',
         'f abc xy',
         '  abc f',
         '',
         'foo',
         '']);
    end; // Tests that add only one undo-able step / no need to run again

      InitTestText(1,2, 1,4);
      TestKeyAtSel('full cell', 4,2, 1,2, VK_DELETE, [],
        ['abc foo',
         ' abc xy',
         '  abc ',
         '',
         'foo',
         '']);
      TestKeyAt('full cell(continue, reinsert)', 1,2, VK_A, [],
        ['abc foo',
         'a abc xy',
         '  abc a',
         '',
         'foo',
         '']);

      Fname := 'Replace/Selection';
    // Tests that add only one undo-able step / no need to run again
    if FMaxUndoSteps<=1 then begin
      InitTestText(1,2, 1,4);
      TestKeyAtSel('at start of cell', 3,2, 1,2, VK_L, [],
        ['abc foo',
         'lo abc xy',
         '  abc lo',
         '',
         'foo',
         '']);

      InitTestText(1,2, 1,4);
      TestKeyAtSel('at end of cell', 2,2, 4,2, VK_L, [],
        ['abc foo',
         'fl abc xy',
         '  abc fl',
         '',
         'foo',
         '']);

      InitTestText(1,2, 1,4);
      TestKeyAtSel('full cell', 4,2, 1,2, VK_L, [],
        ['abc foo',
         'l abc xy',
         '  abc l',
         '',
         'foo',
         '']);
    end; // Tests that add only one undo-able step / no need to run again
    {%endregion}

    {%region Edit Edit Cells, with 2 synced cells on same line }
      Fname := 'Two cells on one line';

    // Tests that add only one undo-able step / no need to run again
    if FMaxUndoSteps<=1 then begin
      InitTestText(1,2, 1,4, 1);
      TestKeyAt('insert in 1st cell', 3, 3, VK_X, [],
        ['abc foo',
         'xfoo abc xy',
         '  xfoo xfoo abc',
         '',
         'foo',
         '']);

      InitTestText(1,2, 1,4, 1);
      TestKeyAt('insert in 2nd cell', 7, 3, VK_Y, [],
        ['abc foo',
         'yfoo abc xy',
         '  yfoo yfoo abc',
         '',
         'foo',
         '']);

      InitTestText(1,2, 1,4, 1);
      TestKeyAt('insert in 2nd cell (2)', 10, 3, VK_Y, [],
        ['abc foo',
         'fooy abc xy',
         '  fooy fooy abc',
         '',
         'foo',
         '']);

      InitTestText(1,2, 1,4, 1);
      TestKeyAt('delete in 1st cell', 6, 3, VK_BACK, [],
        ['abc foo',
         'fo abc xy',
         '  fo fo abc',
         '',
         'foo',
         '']);

      InitTestText(1,2, 1,4, 1);
      TestKeyAt('delete in 2nd cell', 7, 3, VK_DELETE, [],
        ['abc foo',
         'oo abc xy',
         '  oo oo abc',
         '',
         'foo',
         '']);

      InitTestText(1,2, 1,4, 1);
      TestKeyAtSel('Sel-Replace in 1st cell', 4,3, 6,3, VK_M, [],
        ['abc foo',
         'fm abc xy',
         '  fm fm abc',
         '',
         'foo',
         '']);

      InitTestText(1,2, 1,4, 1);
      TestKeyAtSel('Sel-Replace in 2nd cell', 7,3, 9,3, VK_N, [],
        ['abc foo',
         'no abc xy',
         '  no no abc',
         '',
         'foo',
         '']);
    end; // Tests that add only one undo-able step / no need to run again
    {%endregion}

    {%region Insert, delete linebreak in cell (with/without auto-indent) }
      FName := 'LineBreaks';
      InitTestText(1,2, 1,4);
      TestKeyAt('Return middle of FOO, no indent', 2, 2, VK_RETURN, [],
        ['abc foo',
         'f','oo abc xy',
         '  abc f','oo',
         '',
         'foo',
         '']);
      TestKeyAt('Return middle of FOO, no indent - continue 1', 2, 2, VK_A, [],
        ['abc foo',
         'fa','oo abc xy',
         '  abc fa','oo',
         '',
         'foo',
         '']);
      TestKeyAt('Return middle of FOO, no indent - continue 2', 1, 3, VK_B, [],
        ['abc foo',
         'fa','boo abc xy',
         '  abc fa','boo',
         '',
         'foo',
         '']);

      InitTestText(1,2, 1,4);
      TestKeyAt('Return begin of FOO, no indent', 1, 2, VK_RETURN, [],
        ['abc foo',
         '','foo abc xy',
         '  abc ','foo',
         '',
         'foo',
         '']);
      TestKeyAt('Return begin of FOO, no indent - cotinue 1', 1, 3, VK_C, [],
        ['abc foo',
         '','cfoo abc xy',
         '  abc ','cfoo',
         '',
         'foo',
         '']);
//if FDoUndo = 0 then begin
// TODO: fails due to trim space
      TestKeyAt('Return begin of FOO, no indent - cotinue 2', 1, 2, VK_D, [],
        ['abc foo',
         'd','cfoo abc xy',
         '  abc d','cfoo',
         '',
         'foo',
         '']);
//end;

      InitTestText(1,2, 1,4);
      TestKeyAt('Return end of FOO, no indent', 4, 2, VK_RETURN, [],
        ['abc foo',
         'foo',' abc xy',
         '  abc foo','',
         '',
         'foo',
         '']);
      TestKeyAt('Return end of FOO, no indent - continue 1', 1, 5, VK_E, [],
        ['abc foo',
         'foo','e abc xy',
         '  abc foo','e',
         '',
         'foo',
         '']);
      TestKeyAt('Return end of FOO, no indent - continue 2', 7, 4, VK_A, [],
        ['abc foo',
         'afoo','e abc xy',
         '  abc afoo','e',
         '',
         'foo',
         '']);

      InitTestText(1,2, 1,4);
      TestKeyAt('Return middle of ABC, with indent', 4, 3, VK_RETURN, [],
        ['abc foo',
         'foo a','  bc xy',
         '  a','  bc foo',
         '',
         'foo',
         '']);
      TestKeyAt('Return middle of ABC, with indent - continue 1', 5, 3, VK_A, [],
        ['abc foo',
         'foo a','  bca xy',
         '  a','  bca foo',
         '',
         'foo',
         '']);
      TestKeyAt('Return middle of ABC, with indent - continue 2', 6, 2, VK_B, [],
        ['abc foo',
         'foo ab','  bca xy',
         '  ab','  bca foo',
         '',
         'foo',
         '']);

      InitTestText(1,2, 1,4);
      TestKeyAt('Return begin of ABC, with indent', 3, 3, VK_RETURN, [],
        ['abc foo',
         'foo ','  abc xy',
         '  ','  abc foo',
         '',
         'foo',
         '']);
      TestKeyAt('Return begin of ABC, with indent - continue 1', 5, 2, VK_C, [],
        ['abc foo',
         'foo c','  abc xy',
         '  c','  abc foo',
         '',
         'foo',
         '']);
      TestKeyAt('Return begin of ABC, with indent - continue 2', 6, 3, VK_D, [],
        ['abc foo',
         'foo c','  abcd xy',
         '  c','  abcd foo',
         '',
         'foo',
         '']);

      InitTestText(1,2, 1,4);
      TestKeyAt('Return end of ABC, with indent', 6, 3, VK_RETURN, [],
        ['abc foo',
         'foo abc','   xy',
         '  abc','   foo',
         '',
         'foo',
         '']);
      TestKeyAt('Return end of ABC, with indent - continue 1', 6, 4, VK_E, [],
        ['abc foo',
         'foo abce','   xy',
         '  abce','   foo',
         '',
         'foo',
         '']);
      TestKeyAt('Return end of ABC, with indent - continue 2', 3, 5, VK_F, [],
        ['abc foo',
         'foo abce','  f xy',
         '  abce','  f foo',
         '',
         'foo',
         '']);

      InitTestText(1,2, 1,4, 1);
      TestKeyAt('2 cells in one line: insert in 1st cell', 4, 3, VK_RETURN, [],
        ['abc foo',
         'f','  oo abc xy',
         '  f','  oo f','  oo abc',
         '',
         'foo',
         '']);

      InitTestText(1,2, 1,4, 1);
      TestKeyAt('2 cells in one line: insert in 2nd cell', 8, 3, VK_RETURN, [],
        ['abc foo',
         'f','  oo abc xy',
         '  f','  oo f','  oo abc',
         '',
         'foo',
         '']);
    {%endregion}

    {%region Edit just before/after cell, while caret in cell (backspace at start / del at end) }
      Fname := 'Delete/KeyPress caret in cell, text out of cell';
    // Tests that add only one undo-able step / no need to run again
    if FMaxUndoSteps<=1 then begin
      InitTestText(1,2, 1,4);
      TestKeyAt('Delete after  Cell ABC', 8, 2, VK_DELETE, [],
        ['abc foo',
         'foo abcxy',
         '  abc foo',
         '',
         'foo',
         ''], FALSE);

      InitTestText(1,2, 1,4);
      TestKeyAt('Delete before Cell ABC', 5, 2, VK_BACK, [],
        ['abc foo',
         'fooabc xy',
         '  abc foo',
         '',
         'foo',
         ''], FALSE);

      InitTestText(1,2, 1,4);
      TestKeyAtSel('prepare empty cell', 5,2, 8,2, VK_BACK, [],
        ['abc foo',
         'foo  xy',
         '   foo',
         '',
         'foo',
         ''], TRUE);
      TestKeyAt('Backspace, start of empty cell', 5,2, VK_BACK, [],
        ['abc foo',
         'foo xy',
         '   foo',
         '',
         'foo',
         ''], FALSE);
    end; // Tests that add only one undo-able step / no need to run again

      InitTestText(1,2, 1,4);
      TestKeyAtSel('prepare empty cell', 5,2, 8,2, VK_BACK, [],
        ['abc foo',
         'foo  xy',
         '   foo',
         '',
         'foo',
         ''], TRUE);
      TestKeyAt('Delete, end of empty cell', 5,2, VK_DELETE, [],
        ['abc foo',
         'foo xy',
         '   foo',
         '',
         'foo',
         ''], FALSE);
    {%endregion}

    {%region Edit just before/after cell, while caret in cell (selection outside cell / replace, delete) }
      Fname := 'Delete/Selection: caret in cell, text out of cell';
    // Tests that add only one undo-able step / no need to run again
    if FMaxUndoSteps<=1 then begin
      InitTestText(1,2, 1,4);
      TestKeyAtSel('Delete-Sel, after cell', 9,2, 8,2, VK_DELETE, [],
        ['abc foo',
         'foo abcxy',
         '  abc foo',
         '',
         'foo',
         ''], FALSE);

      InitTestText(1,2, 1,4);
      TestKeyAtSel('Replace-Sel, after cell', 9,2, 8,2, VK_M, [],
        ['abc foo',
         'foo abcmxy',
         '  abc foo',
         '',
         'foo',
         ''], FALSE);

      InitTestText(1,2, 1,4);
      TestKeyAtSel('Delete-Sel, part-in,part-after cell', 9,2, 7,2, VK_DELETE, [],
        ['abc foo',
         'foo abxy',
         '  abc foo',
         '',
         'foo',
         ''], FALSE);

      InitTestText(1,2, 1,4);
      TestKeyAtSel('Delete-Sel, part-in,part-after cell (caret out of cell)', 7,2, 9,2, VK_DELETE, [],
        ['abc foo',
         'foo abxy',
         '  abc foo',
         '',
         'foo',
         ''], FALSE);

      InitTestText(1,2, 1,4);
      TestKeyAtSel('Delete-Sel, before cell', 4,2, 5,2, VK_DELETE, [],
        ['abc foo',
         'fooabc xy',
         '  abc foo',
         '',
         'foo',
         ''], FALSE);

      InitTestText(1,2, 1,4);
      TestKeyAtSel('Replace-Sel, before cell', 4,2, 5,2, VK_M, [],
        ['abc foo',
         'foomabc xy',
         '  abc foo',
         '',
         'foo',
         ''], FALSE);

      InitTestText(1,2, 1,4);
      TestKeyAtSel('Delete-Sel, part-in,part-before cell', 4,2, 6,2, VK_DELETE, [],
        ['abc foo',
         'foobc xy',
         '  abc foo',
         '',
         'foo',
         ''], FALSE);

      InitTestText(1,2, 1,4);
      TestKeyAtSel('Delete-Sel, part-in,part-before cell (caret out of cell)', 6,2, 4,2, VK_DELETE, [],
        ['abc foo',
         'foobc xy',
         '  abc foo',
         '',
         'foo',
         ''], FALSE);
    end; // Tests that add only one undo-able step / no need to run again
    {%endregion}


    //Edit just before/after cell, while caret in cell (selection outside cell / replace, delete)
    //  - same with emptied cell
    //  - part in/ part out of cell

    {%region External Edit outside cell }
      Fname := 'External Edit outside cell';
      InitTestText(1,2, 1,4);
      SetCaret(4,3); // into cell def
      FSyncroModule.IncExternalEditLock;
      SynEdit.TextBetweenPoints[Point(1,3), Point(2,3)] := 'a';
      FSyncroModule.DecExternalEditLock;
      TestKeyAt('with cell active', 3, 3, VK_B, [],
        ['abc foo',
         'foo babc xy',
         'a babc foo',
         '',
         'foo',
         '']);

      InitTestText(1,2, 1,4);
      SetCaret(1,3); // out of cell abc
      FSyncroModule.IncExternalEditLock;
      SynEdit.TextBetweenPoints[Point(1,3), Point(2,3)] := 'a';
      FSyncroModule.DecExternalEditLock;
      TestKeyAt('with cell not active', 3, 3, VK_B, [],
        ['abc foo',
         'foo babc xy',
         'a babc foo',
         '',
         'foo',
         '']);

(*
      InitTestText(1,2, 1,4);
      SetCaret(1,3); // out of cell abc
      FSyncroModule.IncExternalEditLock;
      SynEdit.TextBetweenPoints[Point(3,3), Point(3,3)] := 'a';
      FSyncroModule.DecExternalEditLock;
      TestKeyAt('with cell not active, extern in cell', 3, 3, VK_C, [],
        ['abc foo',
         'foo caabc xy',
         '  caabc foo',
         '',
         'foo',
         '']);
*)

    {%endregion}

    // Test UNDO, going back before template started
    ReCreateEdit;
    SynEdit.Options := SynEdit.Options - [eoGroupUndo, eoSmartTabDelete, eoSmartTabs] + [eoAutoIndent];
    SetTestText(0);
    TestKeyAt('key before active', 1, 1, VK_1, [],
      ['1abc foo',
       'foo abc xy',
       '  abc foo',
       '',
       'foo',
       ''],
      False);
    SetCaretAndSel(1,2, 1,4);
    StartSyncroMode;
    TestKeyAt('Insert at Cell FOO (1 of 2), pos=1', 1, 2, VK_M, [],
      ['1abc foo',
       'mfoo abc xy',
       '  abc mfoo',
       '',
       'foo',
       '']);


    //External Edit inside current/other cell (or while caret outside any cell)

    //Application.ProcessMessages;
    //Application.ProcessMessages; sleep(2500);

  end;

var
  m: Integer;
begin
  FCurModule := @FSyncroModule;
  FMaxUndoSteps := 1;

  FDoUndo := 0;
  PushBaseName('simple');
  TestSyncro;

  for m := 1 to 5 do begin
    FMaxUndoSteps := m;

    FDoUndo := 1;
    PopPushBaseName('undo/redo');
    TestSyncro;

    FDoUndo := 2;
    PopPushBaseName('undo/redo');
    TestSyncro;

    FDoUndo := 5;
    PopPushBaseName('undo/redo');
    TestSyncro;
  end;
end;

procedure TTestSyncroEdit.SyncroTemplateEdit;

  procedure SetTestText(TextIdx: Integer = 0);
  begin
    case TextIdx of
      0: SetLines(['abc foo',     // 1
                   'foo property Name: TType read GetName write SetName; xy',  // 2
                   'foo'          // 3
                  ]);
    end;
  end;
  procedure InitTestText(ACells: array of TCellData; TextIdx: Integer = 0);
  var
    Cells: TSynPluginSyncronizedEditList;
    i: Integer;
  begin
    if TextIdx >= 0 then begin
      ReCreateEdit;
      SynEdit.Options := SynEdit.Options - [eoGroupUndo, eoSmartTabDelete, eoSmartTabs] + [eoAutoIndent];
      SetTestText(TextIdx);
    end;

    Cells := TSynPluginSyncronizedEditList.Create;
    for i := 0 to Length(ACells) - 1 do begin
      with Cells.AddNew do begin
        Group    := ACells[i].Group;
        LogStart := ACells[i].PStart;
        LogEnd   := ACells[i].PEnd;
      end;
    end;

    FTemplateModule.CellParserEnabled := False;
    SynEdit.BlockBegin := Point(1, 1);
    FTemplateModule.SetTemplate('', Point(1,1));
    FTemplateModule.AddEditCells(Cells);
    Cells.Free;
  end;

  procedure TestTemplate;
  begin
// TODO: replace selection with new text (2 undo items at once)
    FName := 'Template';
    InitTestText([c(14,2, 18,2, 1),  c(20,2, 25,2, 2),  c(31,2, 34,2, 3),  c(34,2, 38,2, 1),  c(45,2, 48,2, 4),  c(48,2, 52,2, 1)]);
    TestKeyAt('Start of Cell 0  "Name"', 14,2, VK_1, [],
     ['abc foo',
      'foo property 1Name: TType read Get1Name write Set1Name; xy',
      'foo',
      ''], True,
      0,
      cs([c(14,2, 19,2, 1),  c(21,2, 26,2, 2),  c(32,2, 35,2, 3),  c(35,2, 40,2, 1),  c(47,2, 50,2, 4),  c(50,2, 55,2, 1)])
      );
    SetCarretAndCell(19,2, 0);
    TestKeyAt('End of Cell 0 "Name"', 19,2, VK_2, [],
     ['abc foo',
      'foo property 1Name2: TType read Get1Name2 write Set1Name2; xy',
      'foo',
      ''], True,
      0,
      cs([c(14,2, 20,2, 1),  c(22,2, 27,2, 2),  c(33,2, 36,2, 3),  c(36,2, 42,2, 1),  c(49,2, 52,2, 4),  c(52,2, 58,2, 1)])
      );
    SetCarretAndCell(36,2, 2);
    TestKeyAt('End of Cell 2 "Get"', 36,2, VK_3, [],
     ['abc foo',
      'foo property 1Name2: TType read Get31Name2 write Set1Name2; xy',
      'foo',
      ''], True,
      2,
      cs([c(14,2, 20,2, 1),  c(22,2, 27,2, 2),  c(33,2, 37,2, 3),  c(37,2, 43,2, 1),  c(50,2, 53,2, 4),  c(53,2, 59,2, 1)])
      );
    SetCarretAndCell(37,2, 3);
    TestKeyAt('Start of Cell 3 "Name" after "Get"', 37,2, VK_4, [],
     ['abc foo',
      'foo property 41Name2: TType read Get341Name2 write Set41Name2; xy',
      'foo',
      ''], True,
      3,
      cs([c(14,2, 21,2, 1),  c(23,2, 28,2, 2),  c(34,2, 38,2, 3),  c(38,2, 45,2, 1),  c(52,2, 55,2, 4),  c(55,2, 62,2, 1)])
      );
    SetCarretAndCell(55,2, 4);
    TestKeyAt('End of Cell 4 "Set"', 55,2, VK_5, [],
     ['abc foo',
      'foo property 41Name2: TType read Get341Name2 write Set541Name2; xy',
      'foo',
      ''], True,
      4
      );
    // replace selectino / add multiple undo steps (del + ins)
    SetCarretAndCell(56,2, 5);
    TestKeyAtSel('Cell 5 "Name"', 56,2, 63, 2, VK_X, [],
     ['abc foo',
      'foo property x: TType read Get3x write Set5x; xy',
      'foo',
      ''], True,
      5
      );
    SetCarretAndCell(44,2, 4);
    TestKeyAtSel('Cell 4 "Set"', 40,2, 44, 2, VK_Y, [],
     ['abc foo',
      'foo property x: TType read Get3x write yx; xy',
      'foo',
      ''], True,
      4
      );
    SetCarretAndCell(41,2, 5);
    TestKeyAtSel('Cell 5 "Name"', 41,2, 42, 2, VK_Z, [],
     ['abc foo',
      'foo property z: TType read Get3z write yz; xy',
      'foo',
      ''], True,
      5
      );
    // deal with empty cells
    SetCarretAndCell(40,2, 4);
    TestCmdAt('empty Cell 4 "Set" ', 40,2, ecDeleteChar,
     ['abc foo',
      'foo property z: TType read Get3z write z; xy',
      'foo',
      ''], True,
      4
      );
    SetCarretAndCell(40,2, 4);
    TestKeyAt('re-add Cell 4 "Set" ', 40,2, VK_A, [],
     ['abc foo',
      'foo property z: TType read Get3z write az; xy',
      'foo',
      ''], True,
      4
      );
    SetCarretAndCell(41,2, 5);
    TestCmdAt('empty Cell 5 "Name" ', 41,2, ecDeleteChar,
     ['abc foo',
      'foo property : TType read Get3 write a; xy',
      'foo',
      ''], True,
      5
      );
    SetCarretAndCell(38,2, 4);
    TestCmdAt('empty Cell 4 "Set" ', 38,2, ecDeleteChar,
     ['abc foo',
      'foo property : TType read Get3 write ; xy',
      'foo',
      ''], True,
      4
      );
    SetCarretAndCell(38,2, 5);
    TestKeyAt('re-add Cell 5 "Name"', 38,2, VK_C, [],
     ['abc foo',
      'foo property c: TType read Get3c write c; xy',
      'foo',
      ''], True,
      5
      );
    SetCarretAndCell(40,2, 4);
    TestKeyAt('re-add Cell 4 "Set"', 40,2, VK_D, [],
     ['abc foo',
      'foo property c: TType read Get3c write dc; xy',
      'foo',
      ''], True,
      4
      );


    InitTestText([c(14,2, 18,2, 1),  c(20,2, 25,2, 2),  c(31,2, 34,2, 3),  c(34,2, 38,2, 1),  c(45,2, 48,2, 4),  c(48,2, 52,2, 1)]);
    TestKeyAt('', 18,2, VK_1, [],
     ['abc foo',
      'foo property Name1: TType read GetName1 write SetName1; xy',
      'foo',
      '']);
    SetCarretAndCell(35,2, 2);
    TestCmdAt('del Cell 2 "Get" ', 35,2, ecDeleteLastChar,
     ['abc foo',
      'foo property Name1: TType read GeName1 write SetName1; xy',
      'foo',
      ''], True,
      2
      );
    SetCarretAndCell(34,2, 3);
    TestCmdAt('del Cell 3 "Name" ', 34,2, ecDeleteChar,
     ['abc foo',
      'foo property ame1: TType read Geame1 write Setame1; xy',
      'foo',
      ''], True,
      3
      );


    InitTestText([c(14,2, 18,2, 1),  c(20,2, 25,2, 2),  c(31,2, 34,2, 3),  c(34,2, 38,2, 1),  c(45,2, 48,2, 4),  c(48,2, 52,2, 1)]);
    TestKeyAt('', 34,2, VK_1, [],
     ['abc foo',
      'foo property Name: TType read Get1Name write SetName; xy',
      'foo',
      '']);

    // Test UNDO, going back before template started
    ReCreateEdit;
    SynEdit.Options := SynEdit.Options - [eoGroupUndo, eoSmartTabDelete, eoSmartTabs] + [eoAutoIndent];
    SetTestText(0);
    TestKeyAt('Key before template', 2,1, VK_9, [],
     ['a9bc foo',
      'foo property Name: TType read GetName write SetName; xy',
      'foo',
      ''], False);
    InitTestText([c(14,2, 18,2, 1),  c(20,2, 25,2, 2),  c(31,2, 34,2, 3),  c(34,2, 38,2, 1),  c(45,2, 48,2, 4),  c(48,2, 52,2, 1)], -1);
    TestKeyAt('Key in template', 34,2, VK_1, [],
     ['a9bc foo',
      'foo property Name: TType read Get1Name write SetName; xy',
      'foo',
      '']);


  end;

var
  m: Integer;
begin
  FCurModule := @FTemplateModule;
  FMaxUndoSteps := 1;

  FDoUndo := 0;
  PushBaseName('simple');
  TestTemplate;

  for m := 1 to 5 do begin
    FMaxUndoSteps := m;

    FDoUndo := 1;
    PopPushBaseName('undo/redo');
    TestTemplate;

    FDoUndo := 2;
    PopPushBaseName('undo/redo');
    TestTemplate;

    FDoUndo := 5;
    PopPushBaseName('undo/redo');
    TestTemplate;
  end;
end;

initialization

  RegisterTest(TTestSyncroEdit); 
end.

