(*
  .name       Texts
  .task       text management
  .release    1.0
  .language   Oberon-2
  .translator Amiga Oberon 3.2
  .system     AmigaOS 2.04+
  .author     Joachim Barheine
  .address    Hochgrevestraße 3, D-38640 Goslar
  .copyright  (c) 1995 by Joachim Barheine
*)

(* .info: 17/03/95, 14:29:57, version 130 *)

MODULE Texts;  (* $JOIN TextAsm.o *)

IMPORT
  SYS:= SYSTEM,

  ASCII,
  BM:= Bookmarks,
  EIcon,
  Err:= ErrCodes,
  Exec,
  F:= Files,
  K:= Kernel,
  L:= UntracedLists,
  IO:= IOServer,
  S:= Strings,
  Str:= StrPool,
  Sett:= Settings,
  Util:= Utility;

CONST
  noFold* = 0;
  closedFold* = 1;    (* IsFold *)
  openFold* = 2;
  openFoldHeader* = 3;

  noUpdate = -1;

  midGapLen= 4096;

TYPE
  (* -- text folds -- *)

  OpenFoldsIRMem* = UNTRACED POINTER TO ARRAY OF LONGINT;

  Fold = UNTRACED POINTER TO FoldDesc;
  FoldDesc = RECORD (BM.DataDesc)
    begin, end: LONGINT;        (* position of first, last character *)
    text: K.DynString;          (* folded text *)
    bookmarks: BM.BookmarkStorage;      (* bookmark storage *)
    closed: BOOLEAN;
  END;

  (* -- undo buffer -- *)

  Op = UNTRACED POINTER TO OpDesc;
  OpDesc = RECORD (K.ANYDesc)
    pos: LONGINT;
    fold: Fold;             (* Undo/Redo: fold to open before/close after operation *)
  END;

  DelOp = UNTRACED POINTER TO DelOpDesc;
  DelOpDesc = RECORD (OpDesc)
    len: LONGINT;
  END;

  InsOp = UNTRACED POINTER TO InsOpDesc;
  InsOpDesc = RECORD (OpDesc)
    text: K.DynString;
  END;

  ReplChOp = UNTRACED POINTER TO ReplChOpDesc;
  ReplChOpDesc = RECORD (OpDesc)
    char: CHAR;
  END;

  ChainOp = UNTRACED POINTER TO ChainOpDesc;
  ChainOpDesc = RECORD (OpDesc)         (* ignores op.fold and op.pos *)
    opChain: K.DynArray;
    changes: INTEGER;
  END;

  (* -- search -- *)

  JumpTable* = UNTRACED POINTER TO ARRAY ORD(MAX(CHAR)) + 1 OF INTEGER;

  (* -- text class -- *)

  Text* = UNTRACED POINTER TO TextDesc;
  TextDesc* = RECORD (IO.TextDesc)
    buf: K.DynString;                   (* data *)
    name- : ARRAY F.nameLen OF CHAR;    (* text name *)
    chgs0, maxChgs: INTEGER;            (* first change, max. change *)
    linkChgs: INTEGER;                  (* < 0 means to link changes *)
    changes- : INTEGER;                 (* number of changes (user-perspective) *)
    lastChange: LONGINT;                (* position of last change *)
    lcFold: Fold;                       (* closed fold/lastChange *)
    len- : LONGINT;                     (* bytes used *)
    lines- : LONGINT;                   (* total lines *)
    gapPos: LONGINT;                    (* gap position *)
    bookmarks: BM.BookmarkList;         (* bookmarks *)
    undoBuf: K.DynArray;                (* undo operations *)
    undoBufSize: INTEGER;               (* max. undos *)
    foldList: L.List;                   (* list of text folds *)
    folds- : LONGINT;                   (* number of folds *)
    foldedLines- : LONGINT;             (* number of folded lines *)
    pat- , icPat: K.DynString;
    forwJmp, forwICJmp, backJmp, backICJmp: JumpTable;
    patLen- : INTEGER;
  END;

  GetChProc= PROCEDURE (t{8}: Text; pos{0}: LONGINT): CHAR;

(* -- forward declarations -- *)

PROCEDURE^ XInsert(t: Text; pos: LONGINT; VAR text: ARRAY OF CHAR; len: LONGINT);
PROCEDURE^ XDelete(t: Text; pos, len: LONGINT);
PROCEDURE^ (t: Text) Insert* (pos: LONGINT; text: ARRAY OF CHAR; len: LONGINT);
PROCEDURE^ (t: Text) PutCh(pos: LONGINT; c: CHAR);
PROCEDURE^ (t: Text) GetPosLine* (pos: LONGINT; p0, l0: LONGINT): LONGINT;
PROCEDURE^ (t: Text) LineEnd* (pos: LONGINT): LONGINT;
PROCEDURE^ (t: Text) LineBegin* (pos: LONGINT): LONGINT;
PROCEDURE^ (t: Text) LinkChanges*;
PROCEDURE^ (t: Text) LinkChangesDone*;
PROCEDURE^ (t: Text) CloseFold* (pos: LONGINT): BOOLEAN;
PROCEDURE^ (t: Text) OpenFold* (pos: LONGINT): BOOLEAN;
PROCEDURE^ (t: Text) ResolveFold* (pos: LONGINT): BOOLEAN;
PROCEDURE^ XOpenFold(t: Text; fold: Fold; undoFrom, redoFrom: INTEGER);

(* -- operation object -- *)

PROCEDURE (op: Op) Undo(t: Text);

BEGIN
  IF op.fold # NIL THEN
    XOpenFold(t, op.fold, noUpdate, t.changes + 1);
    INC(op.pos, op.fold.begin);
    op.fold:= NIL;
  END;
END Undo;

PROCEDURE (op: Op) Redo(t: Text);

BEGIN
  IF op.fold # NIL THEN
    XOpenFold(t, op.fold, t.changes, noUpdate);
    INC(op.pos, op.fold.begin);
    op.fold:= NIL;
  END;
END Redo;

PROCEDURE (op: Op) NotifyCloseFold(fold: Fold; VAR begin, end: LONGINT;
                                   hidden: LONGINT): BOOLEAN;
END NotifyCloseFold;

PROCEDURE (op: Op) NotifyOpenFold(fold: Fold; VAR begin, end: LONGINT;
                                  hidden: LONGINT): BOOLEAN;
END NotifyOpenFold;

(* -- *)

PROCEDURE (o: InsOp) Dispose* ;

BEGIN
  DISPOSE(o.text);
END Dispose;

PROCEDURE (op: InsOp) Undo(t: Text);

BEGIN
  op.Undo^(t);
  XInsert(t, op.pos, op.text^, LEN(op.text^));
END Undo;

PROCEDURE (op: InsOp) Redo(t: Text);

BEGIN
  op.Redo^(t);
  XInsert(t, op.pos, op.text^, LEN(op.text^));
END Redo;

PROCEDURE (op: InsOp) NotifyCloseFold(fold: Fold; VAR begin, end: LONGINT;
                                      hidden: LONGINT): BOOLEAN;

VAR
  l: LONGINT;

BEGIN
  l:= 0;
  IF op.fold = NIL THEN
    IF op.pos < begin THEN                      (* insert before fold *)
      l:= LEN(op.text^);
    ELSIF op.pos > end THEN
      DEC(op.pos, hidden);
    ELSE
      DEC(op.pos, begin);
      op.fold:= fold;
      RETURN FALSE;             (* done: fold has to be opened *)
    END;
  ELSIF op.fold.begin < fold.begin THEN
    l:= LEN(op.text^) + LEN(op.fold.text^);
  END;
  INC(begin, l); INC(end, l);
  RETURN TRUE;
END NotifyCloseFold;

PROCEDURE (op: InsOp) NotifyOpenFold(fold: Fold; VAR begin, end: LONGINT;
                                     hidden: LONGINT): BOOLEAN;

VAR
  l: LONGINT;

BEGIN
  l:= 0;
  IF op.fold = fold THEN
    INC(op.pos, begin);
    op.fold:= NIL;
    RETURN FALSE;
  ELSIF op.fold = NIL THEN
    IF op.pos < begin THEN                   (* insert before fold *)
      l:= LEN(op.text^);
    ELSIF op.pos > end THEN
      INC(op.pos, hidden);
    END;
  ELSIF op.fold.begin < fold.begin THEN
    l:= LEN(op.text^) + LEN(op.fold.text^);
  END;
  INC(begin, l); INC(end, l);
  RETURN TRUE;
END NotifyOpenFold;

(* -- *)

PROCEDURE (op: DelOp) Undo(t: Text);

BEGIN
  op.Undo^(t);
  XDelete(t, op.pos, op.len);
END Undo;

PROCEDURE (op: DelOp) Redo(t: Text);

BEGIN
  op.Redo^(t);
  XDelete(t, op.pos, op.len);
END Redo;

PROCEDURE (op: DelOp) NotifyCloseFold(fold: Fold; VAR begin, end: LONGINT;
                                      hidden: LONGINT): BOOLEAN;

VAR
  l: LONGINT;

BEGIN
  l:= 0;
  IF op.fold = NIL THEN
    IF op.pos + op.len < begin THEN
      l:= op.len;
    ELSIF op.pos > end THEN
      DEC(op.pos, hidden);
    ELSE
      DEC(op.pos, begin);
      op.fold:= fold;
      RETURN FALSE;
    END;
  ELSIF op.fold.begin < fold.begin THEN
    l:= op.len - LEN(op.fold.text^);
  END;
  DEC(begin, l); DEC(end, l);
  RETURN TRUE;
END NotifyCloseFold;

PROCEDURE (op: DelOp) NotifyOpenFold(fold: Fold; VAR begin, end: LONGINT;
                                     hidden: LONGINT): BOOLEAN;

VAR
  l: LONGINT;

BEGIN
  l:= 0;
  IF op.fold = fold THEN
    INC(op.pos, begin);
    op.fold:= NIL;
    RETURN FALSE;
  ELSIF op.fold = NIL THEN
    IF op.pos + op.len < begin THEN
      l:= op.len;
    ELSIF op.pos > end THEN
      INC(op.pos, hidden);
    END;
  ELSIF op.fold.begin < fold.begin THEN
    l:= op.len - LEN(op.fold.text^);
  END;
  DEC(begin, l); DEC(end, l);
  RETURN TRUE;
END NotifyOpenFold;

(* -- *)

PROCEDURE (op: ReplChOp) Undo(t: Text);

BEGIN
  op.Undo^(t);
  t.PutCh(op.pos, op.char);
END Undo;

PROCEDURE (op: ReplChOp) Redo(t: Text);

BEGIN
  op.Redo^(t);
  t.PutCh(op.pos, op.char);
END Redo;

PROCEDURE (op: ReplChOp) NotifyCloseFold(fold: Fold; VAR begin, end: LONGINT;
                                         hidden: LONGINT): BOOLEAN;

VAR
  l: LONGINT;

BEGIN
  IF op.fold = NIL THEN
    IF op.pos > end THEN
      DEC(op.pos, hidden);
    ELSIF op.pos >= begin THEN
      DEC(op.pos, begin);
      op.fold:= fold;
      RETURN FALSE;
    END;
  ELSIF op.fold.begin < fold.begin THEN
    l:= LEN(op.fold.text^);
    INC(begin, l); INC(end, l);
  END;
  RETURN TRUE;
END NotifyCloseFold;

PROCEDURE (op: ReplChOp) NotifyOpenFold(fold: Fold; VAR begin, end: LONGINT;
                                        hidden: LONGINT): BOOLEAN;

VAR
  l: LONGINT;

BEGIN
  IF op.fold = fold THEN
    INC(op.pos, begin);
    op.fold:= NIL;
    RETURN FALSE;
  ELSIF op.fold = NIL THEN
    IF op.pos > end THEN
      DEC(op.pos, hidden);
    END;
  ELSIF op.fold.begin < fold.begin THEN
    l:= LEN(op.fold.text^);
    INC(begin, l); INC(end, l);
  END;
  RETURN TRUE;
END NotifyOpenFold;

(* -- *)

PROCEDURE (o: ChainOp) Dispose* ;

BEGIN
  o.opChain.Dispose;
END Dispose;

PROCEDURE (op: ChainOp) Undo(t: Text);

VAR
  iop: Op;

BEGIN
  DEC(t.linkChgs);
  WHILE op.changes > 0 DO
    DEC(op.changes);
    iop:= op.opChain.Get(op.changes)(Op);
    iop.Undo(t);
  END;
  INC(t.linkChgs);
END Undo;

PROCEDURE (op: ChainOp) Redo(t: Text);

VAR
  iop: Op;

BEGIN
  DEC(t.linkChgs);
  WHILE op.changes < op.opChain.len DO
    iop:= op.opChain.Get(op.changes)(Op);
    iop.Redo(t);
    INC(op.changes);
  END;
  INC(t.linkChgs);
END Redo;

PROCEDURE (op: ChainOp) NotifyCloseFold(fold: Fold; VAR begin, end: LONGINT;
                                        hidden: LONGINT): BOOLEAN;

VAR
  i: INTEGER;

BEGIN
  i:= op.changes - 1;
  WHILE (i >= 0) & op.opChain.Get(i)(Op).NotifyCloseFold(fold, begin, end, hidden) DO
    DEC(i);
  END;
  RETURN i < 0;
END NotifyCloseFold;

PROCEDURE (op: ChainOp) NotifyOpenFold(fold: Fold; VAR begin, end: LONGINT;
                                        hidden: LONGINT): BOOLEAN;

VAR
  i: INTEGER;

BEGIN
  i:= op.changes;
  WHILE (i < op.opChain.len)
        & op.opChain.Get(i)(Op).NotifyOpenFold(fold, begin, end, hidden) DO
    INC(i);
  END;
  RETURN i >= op.opChain.len;
END NotifyOpenFold;

(* -- folds -- *)

(* notify fold that len characters have been inserted at pos *)
PROCEDURE (f: Fold) NotifyInsert(t: Text; pos, len: LONGINT);

BEGIN
  IF pos <= f.end THEN
    IF pos < f.begin THEN
      INC(f.begin, len);
    ELSIF f.closed THEN
      K.Assert(t.OpenFold(f.begin), Err.textsNoOpenFold);
    END;
    INC(f.end, len);
  END;
END NotifyInsert;

(* notify fold that len characters have been deleted beginning from pos *)
PROCEDURE (f: Fold) NotifyDelete(t: Text; pos, len: LONGINT; VAR grow: LONGINT);

BEGIN
  IF pos <= f.end THEN
    IF pos + len < f.begin THEN                 (* delete before fold *)
      DEC(f.begin, len); DEC(f.end, len);
    ELSIF pos + len > f.end THEN                (* delete part of fold *)
      IF f.closed THEN INC(grow, LEN(f.text^)) END;
      IF pos <= f.begin THEN                    (* delete fold entirely *)
        K.Assert(~f.closed OR t.ResolveFold(f.begin), Err.textsNoResolveFold);
      ELSE
        K.Assert(~f.closed OR t.OpenFold(f.begin), Err.textsNoOpenFold);
        f.end:= t.LineEnd(pos-1);
      END;
    ELSE
      IF pos < f.begin THEN f.begin:= t.LineBegin(pos-1) END;
      DEC(f.end, len);
    END;
  END;
END NotifyDelete;

(* -- buffer access: -- *)

PROCEDURE (t: Text) MoveGap(newPos: LONGINT);

BEGIN
  IF newPos > t.gapPos THEN
    K.Copy(t.buf^, t.gapPos + (LEN(t.buf^) - (t.len + 1)),
           t.buf^, t.gapPos, newPos - t.gapPos);
  ELSIF newPos < t.gapPos THEN
    K.Copy(t.buf^, newPos, t.buf^, newPos + (LEN(t.buf^) - (t.len + 1)), t.gapPos - newPos);
  END;
  t.gapPos:= newPos;
END MoveGap;

PROCEDURE (t: Text) SetBufLen(len: LONGINT);

VAR
  buf: K.DynString;

BEGIN
  t.MoveGap(t.len + 1);                        (* move gap beyond text end    *)
  NEW(buf, len);         (* t.buf is not passed as memory allocation may fail *)
  K.Copy(t.buf^, 0, buf^, 0, t.len + 1);  (* keep buffer contents *)
  DISPOSE(t.buf);
  t.buf:= buf;
END SetBufLen;

PROCEDURE (t: Text) RequireBufLen(newLen: LONGINT);

BEGIN
  IF (newLen > LEN(t.buf^)) OR (LEN(t.buf^) - newLen >= 2 * midGapLen) THEN
    t.SetBufLen(newLen + midGapLen);
  END;
END RequireBufLen;

PROCEDURE CountSpc {"TextAsm.CountSpecial"} (buf{8}: ARRAY OF CHAR;
                                             VAR lines{9}, tabs{10}: LONGINT;
                                             begin{0}, len{1}: LONGINT);


PROCEDURE (t: Text) CountSpecial(VAR lines, tabs: LONGINT; begin, len: LONGINT);

BEGIN
  CountSpc(t.buf^, lines, tabs, begin, len);
END CountSpecial;

(* -- folds -- *)

(* deallocate fold list *)
PROCEDURE DisposeFoldList(t: Text);

VAR
  n: L.NodePtr;

BEGIN
  n:= L.RemHead(t.foldList);
  WHILE n # NIL DO
    WITH n: Fold DO
      IF n.closed THEN
        DISPOSE(n.text);
        n.bookmarks.Dispose;
      END;
    END;
    DISPOSE(n);
    n:= L.RemHead(t.foldList);
  END;
END DisposeFoldList;

(* find fold at 'pos' *)
PROCEDURE (t: Text) FindFold(pos: LONGINT): Fold; (* id *)

VAR
  fold: L.NodePtr;

BEGIN
  fold:= t.foldList.head;
  WHILE (fold # NIL) & (pos > fold(Fold).end) DO
    fold:= fold.next;
  END;
  IF (fold # NIL) & (pos >= fold(Fold).begin) THEN
    RETURN fold(Fold);
  ELSE
    RETURN NIL;
  END;
END FindFold;

(* return if 'pos' is part of a fold: noFold/closedFold/openFold/openFoldHeader *)
PROCEDURE (t: Text) IsFold* (pos: LONGINT): SHORTINT;

VAR
  fold: Fold;

BEGIN
  fold:= t.FindFold(pos);
  IF fold = NIL THEN
    RETURN noFold;
  ELSIF fold.closed THEN
    RETURN closedFold;
  ELSIF pos <= t.LineEnd(fold.begin) THEN
    RETURN openFoldHeader;
  ELSE
    RETURN openFold;
  END;
END IsFold;

PROCEDURE (t: Text) GetFoldBounds* (pos: LONGINT; VAR begin, end: LONGINT): BOOLEAN;

VAR
  fold: Fold;

BEGIN
  fold:= t.FindFold(pos);
  IF fold # NIL THEN
    begin:= fold.begin;
    end:= fold.end;
  END;
  RETURN fold # NIL;
END GetFoldBounds;

PROCEDURE (t: Text) ResolveFold* (pos: LONGINT): BOOLEAN;

VAR
  fold: Fold;
  p0, p1: LONGINT;

BEGIN
  fold:= t.FindFold(pos);
  IF fold # NIL THEN
    p0:= fold.begin;
    p1:= fold.end;
    IF fold.closed THEN
      INC(p1, LEN(fold.text^));
      K.Assert(t.OpenFold(pos), Err.textsNoResolveFold);  (* insert text *)
    END;
    L.Remove(t.foldList, fold);
    DEC(t.folds);
    IO.ResolveFold(t, fold.begin, fold.end);
    DISPOSE(fold);
    RETURN TRUE;
  END;
  RETURN FALSE;
END ResolveFold;

PROCEDURE (t: Text) CloseFold* (pos: LONGINT): BOOLEAN;

VAR
  fold: Fold;
  p0, p1, len, lines, tabs: LONGINT;    (* fold *)


  PROCEDURE UpdateUndoBuf(fold: Fold; a, b, h: LONGINT);
                               (* begin, end, hidden *)
  VAR
    i: INTEGER;

  BEGIN
    i:= t.changes - 1;
    WHILE (t.chgs0 + i >= 0) & (t.maxChgs - i < t.undoBufSize)
  & t.undoBuf.Get((t.chgs0 + i) MOD t.undoBufSize)(Op).NotifyCloseFold(fold, a, b, h) DO
      DEC(i);
    END;
  END UpdateUndoBuf;

  PROCEDURE UpdateRedoBuf(fold: Fold; a, b, h: LONGINT);

  VAR
    i: INTEGER;

  BEGIN
    i:= t.changes;
    WHILE (i < t.maxChgs) &
    t.undoBuf.Get((t.chgs0 + i) MOD t.undoBufSize)(Op).NotifyCloseFold(fold, a, b, h) DO
      INC(i);
    END;
  END UpdateRedoBuf;

  PROCEDURE UpdateFolds(hidden: LONGINT);

  VAR
    n: L.NodePtr;

  BEGIN
    n:= fold.next;
    WHILE n # NIL DO
      DEC(n(Fold).begin, hidden); DEC(n(Fold).end, hidden);
      n:= n.next;
    END;
  END UpdateFolds;

  PROCEDURE UpdateLastChange(begin, len: LONGINT);

  BEGIN
    IF t.lastChange >= begin THEN
      IF t.lastChange < begin + len THEN
        t.lcFold:= fold;
      ELSE
        DEC(t.lastChange, len); t.lcFold:= NIL;
      END;
    END;
  END UpdateLastChange;

BEGIN
  fold:= t.FindFold(pos);
  IF (fold # NIL) & ~fold.closed THEN
    p0:= t.LineEnd(fold.begin) + 1; p1:= fold.end; len:= p1 - p0 + 1;
    IF len > 0 THEN
      IF p1 = t.len THEN t.Insert(p1, "\n", 1) END;
      UpdateUndoBuf(fold, fold.begin, fold.end, len);
      UpdateRedoBuf(fold, fold.begin, fold.end, len);
      UpdateFolds(len);
      t.MoveGap(p0 + len);
      t.CountSpecial(lines, tabs, p0, len);
      t.bookmarks.Hide(fold.bookmarks, p0, t.GetPosLine(p0, 0, 0), len, lines, fold);
      UpdateLastChange(p0, len);
      NEW(fold.text, len);
      K.Copy(t.buf^, p0, fold.text^, 0, len);
      fold.end:= p0 - 1; (* new end *)
      fold.closed:= TRUE;
      DEC(t.lines, lines); INC(t.foldedLines, lines);
      DEC(t.len, len);
      DEC(t.gapPos, len);  (* extend gap *)
      t.RequireBufLen(t.len + 1);
      IO.CloseFold(t, fold.begin, p0, len, lines, tabs);    (* update screen *)
      RETURN TRUE;
    ELSE
      K.Assert(t.ResolveFold(fold.begin), Err.textsNoResolveFold);
    END;
  END;
  RETURN FALSE;
END CloseFold;

PROCEDURE XOpenFold(t: Text; fold: Fold; undoFrom, redoFrom: INTEGER);

VAR
  p0, p1, len, lines, tabs: LONGINT;

  PROCEDURE UpdateUndoBuf(fold: Fold; a, b, h: LONGINT);

  VAR
    i: INTEGER;

  BEGIN
    i:= undoFrom;
    IF t.linkChgs >= 0 THEN DEC(i) END;
    WHILE (t.chgs0 + i >= 0) & (t.maxChgs - i < t.undoBufSize)
   & t.undoBuf.Get((t.chgs0 + i) MOD t.undoBufSize)(Op).NotifyOpenFold(fold, a, b, h) DO
      DEC(i);
    END;
  END UpdateUndoBuf;

  PROCEDURE UpdateRedoBuf(fold: Fold; a, b, h: LONGINT);

  VAR
    i: INTEGER;

  BEGIN
    i:= redoFrom;
    WHILE (i < t.maxChgs)
   & t.undoBuf.Get((t.chgs0 + i) MOD t.undoBufSize)(Op).NotifyOpenFold(fold, a, b, h) DO
      INC(i);
    END;
  END UpdateRedoBuf;

  PROCEDURE UpdateFolds(hidden: LONGINT);

  VAR
    n: L.NodePtr;

  BEGIN
    n:= fold.next;
    WHILE n # NIL DO
      INC(n(Fold).begin, hidden); INC(n(Fold).end, hidden);
      n:= n.next;
    END;
  END UpdateFolds;

BEGIN
  len:= LEN(fold.text^); p0:= fold.end + 1; p1:= fold.end + len;
  IF undoFrom # noUpdate THEN UpdateUndoBuf(fold, fold.begin, fold.end, len) END;
  IF redoFrom # noUpdate THEN UpdateRedoBuf(fold, fold.begin, fold.end, len) END;
  UpdateFolds(len);
  t.RequireBufLen((t.len + 1) + len);
  t.MoveGap(p0);
  K.Copy(fold.text^, 0, t.buf^, t.gapPos, len);
  t.CountSpecial(lines, tabs, t.gapPos, len);
  INC(t.lines, lines); DEC(t.foldedLines, lines);
  INC(t.len, len);
  INC(t.gapPos, len);  (* shrink gap *)
  DISPOSE(fold.text);
  fold.text:= NIL;
  fold.closed:= FALSE;
  fold.end:= p1;        (* new end *)
  IF t.lastChange >= p0 THEN
    IF t.lcFold # fold THEN INC(t.lastChange, len) ELSE t.lcFold:= NIL END;
  END;
  t.bookmarks.Show(fold.bookmarks, p0, t.GetPosLine(p0, 0, 0), len, lines);  (* keep bookmarks *)
  IO.OpenFold(t, fold.begin, p0, len, lines, tabs);    (* update screen *)
END XOpenFold;

PROCEDURE (t: Text) OpenFold* (pos: LONGINT): BOOLEAN;

VAR
  fold: Fold;

BEGIN
  fold:= t.FindFold(pos);
  IF (fold # NIL) & fold.closed THEN
    XOpenFold(t, fold, t.changes, t.changes);
    RETURN TRUE;
  END;
  RETURN FALSE;
END OpenFold;

(* $IF M2Amiga THEN *)

(* OpenFold with file position parameter --> text position *)
PROCEDURE (t: Text) OpenFoldFPos* (filePos: LONGINT): LONGINT;

VAR
  n: L.NodePtr;

BEGIN
  n:= t.foldList.head;
  WHILE (n # NIL) & (filePos > n(Fold).begin) DO
    WITH n: Fold DO
      IF n.closed THEN
        IF filePos > n.end+LEN(n.text^) THEN
          DEC(filePos, LEN(n.text^));
        ELSE
          K.Assert(t.OpenFold(n.begin), Err.textsNoOpenFold);
        END;
      END;
    END;
    n:= n.next;
  END;
  RETURN filePos;
END OpenFoldFPos;

(* $END *)

PROCEDURE (t: Text) NewFold* (begin, end: LONGINT; close: BOOLEAN): BOOLEAN;

VAR
  x: L.NodePtr;
  fold: Fold;

  PROCEDURE FreeObsoleteFolds(begin: LONGINT; VAR end: LONGINT);

  VAR
    n, next: L.NodePtr;

  BEGIN
    n:= t.foldList.head;
    WHILE (n # NIL) & (n(Fold).begin <= end) DO
      WITH n: Fold DO
        IF n.end > begin THEN
          IF n.closed THEN INC(end, LEN(n.text^)) END;
          next:= n.next;
          K.Assert(t.ResolveFold(n.begin), Err.textsNoResolveFold);
        ELSE
          next:= n.next;
        END;
      END;
      n:= next;
    END;
  END FreeObsoleteFolds;

  PROCEDURE Enqueue(fold: Fold);

  VAR
    n: L.NodePtr;

  BEGIN
    n:= t.foldList.head;
    WHILE (n # NIL) & (begin > n(Fold).begin) DO n:= n.next END;
    IF n # NIL THEN
      L.AddBefore(t.foldList, fold, n);
    ELSE
      L.AddTail(t.foldList, fold);
    END;
  END Enqueue;

BEGIN
  K.SortL(begin, end);
  begin:= t.LineBegin(begin); end:= t.LineEnd(end);
  IF t.LineEnd(begin) = end THEN RETURN FALSE END;
  FreeObsoleteFolds(begin, end);
  NEW(fold);
  fold.begin:= begin;
  fold.end:= end;
  fold.text:= NIL;
  fold.closed:= FALSE;
  Enqueue(fold);
  INC(t.folds);
  IO.NewFold(t, begin, end);
  K.Assert(~close OR t.CloseFold(begin), Err.textsNoOpenFold);      (* close it *)
  RETURN TRUE;
END NewFold;

PROCEDURE (t: Text) OpenAllFolds*;

VAR
  n: L.NodePtr;

BEGIN
  IO.Busy(t, Str.openingAllFolds^);
  IO.DisplayOff(t);
  n:= t.foldList.head;
  WHILE n # NIL DO
    IF n(Fold).closed & t.OpenFold(n(Fold).begin) THEN END;
    n:= n.next;
  END;
  IO.DisplayOn(t);
  IO.BusyDone(t);
END OpenAllFolds;

PROCEDURE (t: Text) OpenFoldsInRange* (VAR begin, end: LONGINT; VAR mem: OpenFoldsIRMem);

VAR
  n: L.NodePtr;
  i: LONGINT;

BEGIN
  IF t.folds > 0 THEN
    NEW(mem, t.folds);   (* max. *)
    FOR i:= 0 TO t.folds-1 DO mem[i]:= -1 END;
    i:= 0;
    n:= t.foldList.head;
    WHILE n # NIL DO
      WITH n: Fold DO
        IF n.closed & (n.begin <= end) & (n.end >= begin) THEN
          INC(end, LEN(n.text^));
          IF t.OpenFold(n.begin) THEN mem[i]:= n.begin; INC(i); END;
        END;
      END;
      n:= n.next;
    END;
  ELSE
    mem:= NIL;
  END;
END OpenFoldsInRange;

(* mem is only valid if no changes have occured since OpenFoldsInRange() *)
PROCEDURE (t: Text) RecloseFolds* (VAR mem: OpenFoldsIRMem);

VAR
  i: LONGINT;

BEGIN
  IF mem # NIL THEN
    FOR i:= LEN(mem^)-1 TO 0 BY -1 DO
      IF (mem[i] # -1) & t.CloseFold(mem[i]) THEN END;
    END;
    DISPOSE(mem);
  END;
END RecloseFolds;

PROCEDURE (t: Text) CloseAllFolds*;

VAR
  n: L.NodePtr;

BEGIN
  IO.Busy(t, Str.closingAllFolds^);
  IO.DisplayOff(t);
  n:= t.foldList.head;
  WHILE n # NIL DO
    IF ~n(Fold).closed & t.CloseFold(n(Fold).begin) THEN END;
    n:= n.next;
  END;
  IO.DisplayOn(t);
  IO.BusyDone(t);
END CloseAllFolds;

PROCEDURE (t: Text) ResolveAllFolds*;

VAR
  n, next: L.NodePtr;

BEGIN
  IO.Busy(t, Str.resolvingAllFolds^);
  IO.DisplayOff(t);
  next:= t.foldList.head;
  WHILE next # NIL DO
    n:= next; next:= n.next;
    IF t.ResolveFold(n(Fold).begin) THEN END;
  END;
  IO.DisplayOn(t);
  IO.BusyDone(t);
END ResolveAllFolds;

(* -- basic editing operations -- *)

PROCEDURE XInsert(t: Text; pos: LONGINT; VAR text: ARRAY OF CHAR; len: LONGINT);

VAR
  lines, tabs: LONGINT;

  PROCEDURE RecordChanges(pos, len: LONGINT);

  VAR
    delOp: DelOp;
    chainOp: ChainOp;

  BEGIN
    IF t.undoBufSize > 0 THEN
      NEW(delOp);
      delOp.fold:= NIL;
      delOp.pos:= pos;
      delOp.len:= len;
      IF t.linkChgs < 0 THEN
        chainOp:= t.undoBuf.Get((t.chgs0 + t.changes) MOD t.undoBufSize)(ChainOp);
        chainOp.opChain.Put(delOp, chainOp.changes);
      ELSE
        t.undoBuf.Put(delOp, (t.chgs0 + t.changes) MOD t.undoBufSize);
      END;
    END;
    t.lastChange:= pos;
    t.lcFold:= NIL;
  END RecordChanges;

  PROCEDURE UpdateFolds(pos, len: LONGINT);

  VAR
    n: L.NodePtr;

  BEGIN
    n:= t.foldList.head;
    WHILE n # NIL DO n(Fold).NotifyInsert(t, pos, len); n:= n.next END;
  END UpdateFolds;

BEGIN
  UpdateFolds(pos, len);
  RecordChanges(pos, len);
  t.RequireBufLen((t.len + 1) + len);
  t.MoveGap(pos);
  K.Copy(text, 0, t.buf^, pos, len);
  t.CountSpecial(lines, tabs, t.gapPos, len);
  t.bookmarks.NotifyInsert(pos, len, lines);
  INC(t.lines, lines);
  INC(t.len, len);
  INC(t.gapPos, len);
  IO.Insert(t, pos, len, lines, tabs);  (* update screen *)
END XInsert;

PROCEDURE XDelete(t: Text; pos, len: LONGINT);

VAR
  lines, tabs: LONGINT;

  PROCEDURE RecordChanges(pos, len: LONGINT);

  VAR
    insOp: InsOp;
    chainOp: ChainOp;
    i: LONGINT;

  BEGIN
    IF t.undoBufSize > 0 THEN
      NEW(insOp);
      insOp.fold:= NIL;
      insOp.pos:= pos;
      NEW(insOp.text, len);
      K.Copy(t.buf^, pos, insOp.text^, 0, len);
      IF t.linkChgs < 0 THEN
        chainOp:= t.undoBuf.Get((t.chgs0 + t.changes) MOD t.undoBufSize)(ChainOp);
        chainOp.opChain.Put(insOp, chainOp.changes);
      ELSE
        t.undoBuf.Put(insOp, (t.chgs0 + t.changes) MOD t.undoBufSize);
      END;
    END;
    t.lastChange:= pos;
    t.lcFold:= NIL;
  END RecordChanges;

  PROCEDURE UpdateFolds(pos: LONGINT; VAR len: LONGINT);

  VAR
    n, next: L.NodePtr;
    grow: LONGINT;

  BEGIN
    grow:= 0;
    next:= t.foldList.head;
    WHILE next # NIL DO
      n:= next; next:= n.next;
      n(Fold).NotifyDelete(t, pos, len, grow);
    END;
    INC(len, grow);
  END UpdateFolds;

BEGIN
  UpdateFolds(pos, len);
  t.MoveGap(pos + len);
  RecordChanges(pos, len);
  t.CountSpecial(lines, tabs, pos, len);
  t.bookmarks.NotifyDelete(pos, len, lines);
  DEC(t.lines, lines);
  DEC(t.len, len);
  DEC(t.gapPos, len);  (* extend gap *)
  t.RequireBufLen(t.len + 1);
  IO.Delete(t, pos, len, lines, tabs);    (* update screen *)
END XDelete;

PROCEDURE (t: Text) PutCh(pos: LONGINT; c: CHAR);

VAR
  d: LONGINT;

  PROCEDURE RecordChanges;

  VAR
    replChOp: ReplChOp;
    chainOp: ChainOp;

  BEGIN
    IF t.undoBufSize > 0 THEN
      NEW(replChOp);
      replChOp.fold:= NIL;
      replChOp.pos:= pos;
      replChOp.char:= t.buf[pos + d];
      IF t.linkChgs < 0 THEN
        chainOp:= t.undoBuf.Get((t.chgs0 + t.changes) MOD t.undoBufSize)(ChainOp);
        chainOp.opChain.Put(replChOp, chainOp.changes);
      ELSE
        t.undoBuf.Put(replChOp, (t.chgs0 + t.changes) MOD t.undoBufSize);
      END;
    END;
    t.lastChange:= pos;
    t.lcFold:= NIL;
  END RecordChanges;

BEGIN
  IF pos >= t.gapPos THEN d:= LEN(t.buf^) - (t.len + 1) ELSE d:= 0 END;
  RecordChanges;
  t.buf[pos + d]:= c;
  IO.DrawRange(t, pos, pos);
END PutCh;

(* -- undo objects: -- *)


PROCEDURE (t: Text) NextChange;

VAR
  i: INTEGER;
  chainOp: ChainOp;

BEGIN
  IF t.linkChgs < 0 THEN
    IF t.undoBufSize > 0 THEN
      chainOp:= t.undoBuf.Get((t.chgs0 + t.changes) MOD t.undoBufSize)(ChainOp);
      INC(chainOp.changes);
    END;
  ELSE
    INC(t.changes);
    FOR i:= t.changes TO t.maxChgs - 1 DO
      t.undoBuf.Delete((t.chgs0 + i) MOD t.undoBufSize);
    END;
    t.maxChgs:= t.changes;
  END;
END NextChange;

(* $RangeChk- $OvflChk- $NilChk- *)

(* quick get-char function *)
PROCEDURE BGetChar(t{8}: Text; pos{0}: LONGINT): CHAR;

BEGIN
  IF pos >= t.gapPos THEN INC(pos, LEN(t.buf^) - (t.len + 1)) END;
  RETURN t.buf[pos];
END BGetChar;

(* quick get-char function: ignore case *)
PROCEDURE BGetCharIC(t{8}: Text; pos{0}: LONGINT): CHAR;

BEGIN
  IF pos >= t.gapPos THEN INC(pos, LEN(t.buf^) - (t.len + 1)) END;
  RETURN Util.ToUpper(t.buf[pos]);
END BGetCharIC;

(* -- public methods: -- *)

PROCEDURE (t: Text) GetChar* (pos: LONGINT): CHAR;

BEGIN
  RETURN BGetChar(t, pos);
END GetChar;

(* get position of line begin *)
PROCEDURE (t: Text) LineBegin* (pos: LONGINT): LONGINT;

BEGIN
  DEC(pos);
  WHILE (pos >= 0) & (BGetChar(t, pos) # ASCII.lf) DO DEC(pos) END;
  RETURN pos + 1;
END LineBegin;

(* get position of line end *)
PROCEDURE (t: Text) LineEnd* (pos: LONGINT): LONGINT;

BEGIN
  WHILE (pos < t.len) & (BGetChar(t, pos) # ASCII.lf) DO INC(pos) END;
  RETURN pos;
END LineEnd;

(* get length of line incl. linefeed *)
PROCEDURE (t: Text) LineLength* (pos: LONGINT): LONGINT;

BEGIN
  RETURN t.LineEnd(pos) - t.LineBegin(pos) + 1;
END LineLength;

(* $RangeChk= $OvflChk= $NilChk= *)

(* this procedure should be called when the user pauses *)
PROCEDURE (t: Text) Update* (currentPos: LONGINT);

BEGIN
  IF t.gapPos # currentPos THEN
    t.MoveGap(currentPos);
  ELSIF LEN(t.buf^) - (t.len + 1) < midGapLen DIV 2 THEN
    t.SetBufLen((t.len + 1) + midGapLen);
    t.MoveGap(currentPos);
  END;
END Update;

PROCEDURE (t: Text) Read* (pos: LONGINT; VAR text: ARRAY OF CHAR; len: LONGINT);

VAR
  len1: LONGINT;

BEGIN
  IF len > (t.len + 1) - pos THEN len:= (t.len + 1) - pos END;
  IF len > 0 THEN
    IF pos < t.gapPos THEN
      IF pos + len <= t.gapPos THEN
        K.Copy(t.buf^, pos, text, 0, len);
      ELSE
        len1:= t.gapPos - pos;
        K.Copy(t.buf^, pos, text, 0, len1);
        K.Copy(t.buf^, t.gapPos + (LEN(t.buf^) - (t.len + 1)), text, len1, len - len1);
      END;
    ELSE
      K.Copy(t.buf^, pos + (LEN(t.buf^) - (t.len + 1)), text, 0, len);
    END;
  END;
END Read;

PROCEDURE (t: Text) Insert* (pos: LONGINT; text: ARRAY OF CHAR; len: LONGINT);

(* $CopyArrays- *)

BEGIN
  IF len > 0 THEN
    IF pos < 0 THEN pos:= 0 ELSIF pos > t.len THEN pos:= t.len END;
    XInsert(t, pos, text, len);
    t.NextChange;
  END;
END Insert;

PROCEDURE (t: Text) Delete* (pos, len: LONGINT);

BEGIN
  IF len > t.len - pos THEN len:= t.len - pos END;
  IF len > 0 THEN
    XDelete(t, pos, len);
    t.NextChange;
  END;
END Delete;

(* (over-)write a single character except for linefeeds *)
PROCEDURE (t: Text) PutChar* (pos: LONGINT; c: CHAR);

VAR
  s: ARRAY 1 OF CHAR;
  op: Op;

BEGIN
  s[0]:= c;
  IF c = ASCII.ht THEN
    t.LinkChanges;
    t.Insert(pos, s, 1);
    t.Delete(pos+1, 1);
    t.LinkChangesDone;
  ELSIF pos >= t.len THEN
    t.Insert(t.len, s, 1);
  ELSE
    t.PutCh(pos, c);
    t.NextChange;
  END;
END PutChar;

(* -- history: -- *)

PROCEDURE (t: Text) Undo* (): BOOLEAN;

VAR
  iop: Op;

BEGIN
  IF (t.chgs0 + t.changes > 0) & (t.maxChgs - t.changes < t.undoBufSize) THEN
    DEC(t.changes);
    iop:= t.undoBuf.Get((t.chgs0 + t.changes) MOD t.undoBufSize)(Op);
    iop.Undo(t);
    RETURN TRUE;
  ELSE
    RETURN FALSE;
  END;
END Undo;

PROCEDURE (t: Text) Redo* (): BOOLEAN;

VAR
  iop: Op;

BEGIN
  IF (t.changes < t.maxChgs) & (t.linkChgs >= 0) THEN
    iop:= t.undoBuf.Get((t.chgs0 + t.changes) MOD t.undoBufSize)(Op);
    iop.Redo(t);
    INC(t.changes);
    RETURN TRUE;
  ELSE
    RETURN FALSE;
  END;
END Redo;

(* collect several changes into a single: begin *)
PROCEDURE (t: Text) LinkChanges*;

VAR
  chainOp: ChainOp;

BEGIN
  IF t.undoBufSize > 0 THEN
    IF t.linkChgs = 0 THEN
      NEW(chainOp);
      chainOp.fold:= NIL;
      chainOp.pos:= 0;
      chainOp.opChain.New(2, 10);
      chainOp.changes:= 0;
      t.undoBuf.Put(chainOp, (t.chgs0 + t.changes) MOD t.undoBufSize);
    END;
  END;
  DEC(t.linkChgs);
END LinkChanges;

(* collect several changes into a single: end *)
PROCEDURE (t: Text) LinkChangesDone*;

VAR
  op: Op;

BEGIN
  INC(t.linkChgs);
  IF t.linkChgs = 0 THEN t.NextChange END;
END LinkChangesDone;

PROCEDURE (t: Text) RestoreStatus*;

BEGIN
  t.RestoreStatus^;
  WHILE t.linkChgs < 0 DO t.LinkChangesDone END;
END RestoreStatus;

PROCEDURE (t: Text) LastChange* (): LONGINT;

BEGIN
  IF (t.lcFold # NIL) & ~t.OpenFold(t.lcFold.begin) THEN
    RETURN t.gapPos;
  ELSE
    RETURN t.lastChange;
  END;
END LastChange;

PROCEDURE (t: Text) NewBookmark* (pos, line: LONGINT): BM.BookmarkID;  (* id *)

BEGIN
  IF line = K.undef THEN line:= t.GetPosLine(pos, 0, 0) END;
  RETURN t.bookmarks.NewBookmark(pos, line);
END NewBookmark;

PROCEDURE (t: Text) FreeBookmark* (id: BM.BookmarkID);

BEGIN
  t.bookmarks.FreeBookmark(id);
END FreeBookmark;

PROCEDURE (t: Text) GetBookmark* (id: BM.BookmarkID; VAR pos, line: LONGINT);

VAR
  fold: L.NodePtr;

BEGIN
  t.bookmarks.GetBookmark(id, pos, line, fold);
  IF fold # NIL THEN
    K.Assert(t.OpenFold(fold(Fold).begin), Err.textsNoOpenFold);
    t.bookmarks.GetBookmark(id, pos, line, fold);
  END;
END GetBookmark;

(* get line number of a position *)
PROCEDURE (t: Text) GetPosLine* (pos: LONGINT; p0, l0: LONGINT): LONGINT;

VAR
  p: LONGINT;

BEGIN
  t.bookmarks.GetBMFromPos(pos, p0, l0);
  IF p0 > pos THEN
    FOR p:= pos TO p0 - 1 DO
      IF BGetChar(t, p) = ASCII.lf THEN DEC(l0) END;
    END;
  ELSIF p0 < pos THEN
    FOR p:= p0 TO pos - 1 DO
      IF BGetChar(t, p) = ASCII.lf THEN INC(l0) END;
    END;
  END;
  RETURN l0;
END GetPosLine;

(* get position of a line *)
PROCEDURE (t: Text) GetLinePos* (line: LONGINT; p0, l0: LONGINT): LONGINT;

VAR
  l: LONGINT;

BEGIN
  t.bookmarks.GetBMFromLine(line, p0, l0);
  IF l0 > line THEN
    FOR l:= line TO l0 - 1 DO
      p0:= t.LineBegin(p0); DEC(p0);
    END;
  ELSIF l0 < line THEN
    FOR l:= l0 TO line - 1 DO
      p0:= t.LineEnd(p0); INC(p0);
    END;
  END;
  RETURN t.LineBegin(p0);
END GetLinePos;

PROCEDURE (t: Text) Rename* (name: ARRAY OF CHAR);

(* $CopyArrays- *)

BEGIN
  COPY(name, t.name);
  IO.Update(t);
END Rename;

PROCEDURE Init(t: Text);

BEGIN
  t.buf:= NIL;
  t.chgs0:= 0;
  t.changes:= 0;
  t.maxChgs:= 0;
  t.changes:= 0;
  t.lastChange:= 0;
  t.lcFold:= NIL;
  t.undoBuf.New(20, 100);
  t.linkChgs:= 0;
  t.gapPos:= 0;
  t.len:= 0;
  t.lines:= 0;
  t.foldedLines:= 0;
  t.folds:= 0;
  L.Init(t.foldList);
END Init;

(* 'constructor': this has to be called after 'NEW(t)' *)
PROCEDURE (t: Text) New*;

BEGIN
  t.New^;
  Init(t);
  t.bookmarks.New(20, 20);
  t.name:= "";
  IF Sett.rmbChgs THEN t.undoBufSize:= Sett.undoBufSize ELSE t.undoBufSize:= 0 END;
  NEW(t.forwJmp);
  NEW(t.forwICJmp);
  NEW(t.backJmp);
  NEW(t.backICJmp);
END New;

(* 'destructor': this has to be called before 'DISPOSE(t)' *)
PROCEDURE (t: Text) Dispose*;

BEGIN
  DisposeFoldList(t);
  t.bookmarks.Dispose;
  t.undoBuf.Dispose;
  IF t.buf # NIL THEN DISPOSE(t.buf) END;
  DISPOSE(t.forwJmp);
  DISPOSE(t.forwICJmp);
  DISPOSE(t.backJmp);
  DISPOSE(t.backICJmp);
  t.Dispose^;
END Dispose;

(* edit a new text / erase the old *)
PROCEDURE (t: Text) Clear* (name: ARRAY OF CHAR);

(* $CopyArrays- *)

BEGIN
  DisposeFoldList(t);
  IF t.buf # NIL THEN DISPOSE(t.buf) END;
  t.bookmarks.Dispose;
  t.undoBuf.Dispose;
  Init(t);
  t.bookmarks.New(20, 20);
  NEW(t.buf, midGapLen);  (* buffer *)
  COPY(name, t.name);
  t.buf[0]:= ASCII.nul;
  t.gapPos:= 1;
  t.lines:= 1;
  IF Sett.rmbChgs THEN t.undoBufSize:= Sett.undoBufSize ELSE t.undoBufSize:= 0 END;
END Clear;

(* insert a file *)
PROCEDURE (t: Text) InsertFile* (pos: LONGINT; VAR f: F.File): BOOLEAN;

CONST
  bufLen = 8192;

VAR
  done: BOOLEAN;
  buf: UNTRACED POINTER TO ARRAY bufLen OF CHAR;
  max: LONGINT;

BEGIN
  NEW(buf);
  done:= TRUE;
  t.LinkChanges;
  max:= pos + f.len - (f.len MOD bufLen);
  WHILE done & (pos < max) DO
    f.Read(buf^, bufLen);
    done:= done & f.done;
    IF done THEN
      t.Insert(pos, buf^, bufLen);
      INC(pos, bufLen);
    END;
  END;
  IF done THEN
    f.Read(buf^, f.len MOD bufLen);
    IF f.done THEN
      t.Insert(pos, buf^, f.len MOD bufLen);
      done:= TRUE;
    END;
  END;
  t.LinkChangesDone;
  DISPOSE(buf);
  RETURN done;
END InsertFile;

(* load a new text *)
PROCEDURE (t: Text) Load* (VAR f: F.File; name: ARRAY OF CHAR): BOOLEAN;

VAR
  buf: K.DynString;
  lines: LONGINT;
  contBin: BOOLEAN;

  (* count lines and create bookmarks every 256 lines *)
  PROCEDURE CountLines(b: K.DynString; VAR containsBinary: BOOLEAN): LONGINT;

  VAR
    c: CHAR;
    i: LONGINT;
    lines: LONGINT;

  BEGIN
    lines:= 1; containsBinary:= FALSE;
    FOR i:= 0 TO f.len - 1 DO
      (* $RangeChk- $OvflChk- $NilChk- *)
      c:= b[i];
      IF c = ASCII.nul THEN
        b[i]:= ASCII.esc;
        containsBinary:= TRUE;
      ELSIF c = ASCII.lf THEN
        IF lines MOD 256 = 0 THEN
          IF t.NewBookmark(i + 1, lines) = 0 THEN END;
        END;
        INC(lines);
      END;
      (* $RangeChk= $OvflChk= $NilChk= *)
    END;
    RETURN lines;
  END CountLines;

(* $CopyArrays- *)

BEGIN
  NEW(buf, f.len + 1 + midGapLen);
  IF f.len > 0 THEN f.Read(buf^, f.len) END;
  IF f.done THEN
    t.bookmarks.Dispose;   (* bookmarks are lost anyway *)
    t.bookmarks.New(5, 10);
    lines:= CountLines(buf, contBin);
    DisposeFoldList(t);
    t.undoBuf.Dispose;
    IF t.buf # NIL THEN DISPOSE(t.buf) END;
    Init(t);
    t.buf:= buf;
    t.undoBuf.New(20, 50);
    COPY(name, t.name);
    t.buf[f.len]:= ASCII.nul;  (* sentiel *)
    t.gapPos:= f.len + 1;
    t.len:= f.len;
    t.lines:= lines;
    RETURN TRUE;
  END;
  DISPOSE(buf);
  RETURN FALSE;
END Load;

PROCEDURE (t: Text) ParseTooltypes* (VAR tt: EIcon.Tooltypes);

VAR
  str: EIcon.LinePtr;
  i: INTEGER;
  pos: LONGINT;
  begin, end: LONGINT;
  closed: BOOLEAN;

BEGIN
  IF tt # NIL THEN
    i:= 0;
    WHILE tt[i] # NIL DO
      str:= tt[i]; pos:= 0;
      IF K.MatchIC(str^, pos, EIcon.foldTooltype)
         & K.ReadInt(str^, pos, begin) & K.MatchIC(str^, pos, "-")
         & K.ReadInt(str^, pos, end) & K.MatchIC(str^, pos, ":") THEN
        closed:= K.MatchIC(str^, pos, "CLOSED");
        K.SortL(begin, end); K.ClipL(end, 0, t.len); K.ClipL(begin, 0, end);
        IF t.NewFold(begin, end, closed) THEN END;
      ELSIF K.MatchIC(str^, pos, EIcon.positionTooltype)
          & K.ReadInt(str^, pos, t.lastChange) THEN
        K.ClipL(t.lastChange, 0, t.len);
      END;
      INC(i);
    END;
  END;
END ParseTooltypes;

PROCEDURE (t: Text) InitTooltypes* (tt: EIcon.Tooltypes);

VAR
  i: INTEGER;
  offset, lc: LONGINT;
  n: L.NodePtr;
  a: ARRAY 3 OF LONGINT;

BEGIN
  IF ~K.memAlert THEN
    i:= 0; WHILE tt[i] # NIL DO INC(i) END;
    (* mark tooltypes *)
    NEW(tt[i]);
    tt[i]^:= EIcon.edtPrivTooltype;
    INC(i);
    (* save folds *)
    lc:= t.lastChange;
    n:= t.foldList.head;
    offset:= 0;
    WHILE (n # NIL) & (i < LEN(tt^)-2) DO
      WITH n: Fold DO
        a[0]:= n.begin + offset;
        a[1]:= n.end + offset;
        a[2]:= SYS.ADR("OPEN");
        IF (t.lastChange < n.begin) OR (t.lastChange > n.end) THEN
          IF n.begin > t.lastChange THEN lc:= t.lastChange + offset END;
          IF n.closed THEN
            a[2]:= SYS.ADR("CLOSED");
            INC(a[1], LEN(n.text^));
          ELSIF Sett.closeFlds THEN
            a[2]:= SYS.ADR("CLOSED");
            DEC(offset, n.end - t.LineEnd(n.begin));
          END;
        ELSE
          lc:= t.lastChange + offset;
          IF n.closed THEN
            INC(a[1], LEN(n.text^));
            INC(offset, LEN(n.text^));
          END;
        END;
      END;
      NEW(tt[i]);
      K.FormatString(tt[i]^, "FOLDER=%ld-%ld:%s", a);
      n:= n.next; INC(i);
    END;
    (* save position *)
    IF Sett.rmbPos & (lc > 0) THEN
      a[0]:= lc;
      NEW(tt[i]);
      K.FormatString(tt[i]^, "POSITION=%ld", a);
      INC(i);
    END;
    tt[i]:= NIL;
  END;
END InitTooltypes;

(* text may be preallocated *)
PROCEDURE (t: Text) ReadContents* (VAR text: K.DynString; pos, len: LONGINT): LONGINT;

VAR
  i, p0: LONGINT;
  n: L.NodePtr;

  PROCEDURE Write(src: ARRAY OF CHAR; p, l: LONGINT);

  VAR
    old: K.DynString;

  (* $CopyArrays- *)

  BEGIN
    IF i + l > LEN(text^) THEN
      old:= text;
      NEW(text, i + l + 4096);
      K.Copy(old^, 0, text^, 0, i);
      DISPOSE(old);
    END;
    K.Copy(src, p, text^, i, l);
    INC(i, l);
  END Write;

BEGIN
  i:= 0;
  IF len > 0 THEN
    IF pos + len > t.len THEN len:= t.len - pos END;
    IF text = NIL THEN NEW(text, len) END;
    p0:= pos;
    t.MoveGap(pos + len);
    n:= t.foldList.head;
    WHILE (n # NIL) & (n(Fold).end < pos) DO n:= n.next END;
    WHILE (n # NIL) & (n(Fold).begin <= pos + len) & (n(Fold).end < pos + len) DO
      WITH n: Fold DO
        IF n.closed THEN
          Write(t.buf^, p0, n.end - p0 + 1);
          Write(n.text^, 0, LEN(n.text^));
          p0:= n.end + 1;
        END;
      END;
      n:= n.next;
    END;
    IF p0 < pos + len THEN Write(t.buf^, p0, pos + len - p0) END;
  END;
  RETURN i;
END ReadContents;

(* store temporary *)
PROCEDURE (t: Text) StoreTemp* (VAR f: F.File): BOOLEAN;

VAR
  n: L.NodePtr;
  p0: LONGINT;

BEGIN
  t.MoveGap(t.len + 1);
  p0:= 0;
  n:= t.foldList.head;
  WHILE n # NIL DO
    WITH n: Fold DO
      IF n.closed THEN
        IF n.end >= p0 THEN f.WriteFrom(t.buf^, p0, n.end - p0 + 1) END;
        f.Write(n.text^, LEN(n.text^));
        p0:= n.end + 1;
      END;
    END;
    n:= n.next;
  END;
  IF t.len > p0 THEN f.WriteFrom(t.buf^, p0, t.len - p0) END;
  t.buf[t.len]:= ASCII.nul;
  RETURN f.done;
END StoreTemp;

PROCEDURE (t: Text) Store* (VAR f: F.File): BOOLEAN;

VAR
  w: IO.Window;

  PROCEDURE Filter;

  CONST
    tabSpc = "                                ";

  VAR
    c: CHAR;
    i, ii, lineBeg, lastChg: LONGINT;
    tw: INTEGER;

  BEGIN
    IF t.cleanLines OR Sett.convTabs THEN
      lastChg:= t.lastChange;
      t.LinkChanges;
      i:= 0; lineBeg:= 0;
      WHILE i < t.len DO
        c:= BGetChar(t, i);
        IF c = ASCII.lf THEN
          lineBeg:= i + 1;
          ii:= i;
          IF t.cleanLines THEN
            WHILE (ii >= 1) & (BGetChar(t, ii - 1) = " ") DO DEC(ii) END;
            t.Delete(ii, i - ii);  (* remove blanks *)
          END;
          i:= ii + 1;
        ELSIF (c = ASCII.ht) & Sett.convTabs THEN
          tw:= Sett.tabAlign - (SHORT(i - lineBeg) MOD Sett.tabAlign);
          t.Insert(i, tabSpc, tw);
          INC(i, tw);
          t.Delete(i, 1);
        ELSE
          INC(i);
        END;
      END;
      IF t.cleanLines THEN
        ii:= t.len;
        WHILE (ii >= 1) & (BGetChar(t, ii - 1) = ASCII.lf) DO DEC(ii) END;
        t.Delete(ii + 1, t.len - (ii + 1));  (* remove empty lines at text-end *)
      END;
      t.LinkChangesDone;
      t.lastChange:= lastChg;
    END;
  END Filter;

BEGIN
  IF ~K.memAlert THEN Filter END;
  IF t.StoreTemp(f) THEN
    INC(t.chgs0, t.changes);
    DEC(t.maxChgs, t.changes);
    t.changes:= 0;
    IO.Update(t);
  END;
  RETURN f.done;
END Store;

PROCEDURE (t: Text) NewPrefs* ;

BEGIN
  IF Sett.rmbChgs THEN
    IF (Sett.undoBufSize < t.undoBufSize) & (t.chgs0 + t.changes > Sett.undoBufSize) THEN
      t.undoBufSize:= t.chgs0 + t.changes;
    ELSE
      t.undoBufSize:= Sett.undoBufSize;
    END;
  ELSIF t.undoBufSize # 0 THEN
    t.undoBuf.Dispose;
    t.undoBuf.New(1,1);  (* dummy *)
    t.undoBufSize:= 0;
    t.maxChgs:= t.changes;
  END;
END NewPrefs;

(* -- utility functions -- *)

(* is found occurance of 'pat' a word/no substring *)
PROCEDURE IsDelimeter* (t: Text; pos: LONGINT): BOOLEAN;

VAR
  c: CHAR;

BEGIN
  c:= BGetCharIC(t, pos);
  RETURN (c < 0B0X) & ((c < "0") OR (c > "9")) & ((c < "A") OR (c > "Z"));
END IsDelimeter;

(* get position of word begin *)
PROCEDURE (t: Text) WordBegin* (pos: LONGINT): LONGINT;

BEGIN
  WHILE (pos < t.len - 1) & IsDelimeter(t, pos) DO INC(pos) END;
  WHILE (pos >= 0) & ~IsDelimeter(t, pos) DO DEC(pos) END;
  RETURN pos + 1;
END WordBegin;

(* get position of word end *)
PROCEDURE (t: Text) WordEnd* (pos: LONGINT): LONGINT;

BEGIN
  WHILE (pos > 1) & IsDelimeter(t, pos) DO DEC(pos) END;
  WHILE (pos < t.len) & ~IsDelimeter(t, pos) DO INC(pos) END;
  RETURN pos - 1;
END WordEnd;

PROCEDURE (t: Text) NewPattern* (str: ARRAY OF CHAR);

VAR
  c, j: INTEGER;

(* $CopyArrays- *)

BEGIN
  IF t.pat # NIL THEN
    DISPOSE(t.pat); t.pat:= NIL;
    DISPOSE(t.icPat); t.icPat:= NIL;
  END;
  t.patLen:= SHORT(S.Length(str));
  IF t.patLen > 0 THEN
    NEW(t.pat, t.patLen); NEW(t.icPat, t.patLen);
    FOR j:= 0 TO t.patLen -1 DO
      t.pat[j]:= str[j]; t.icPat[j]:= Util.ToUpper(str[j]);
    END;
    FOR c:= 0 TO ORD(MAX(CHAR)) DO
      t.forwJmp[c]:= t.patLen; t.forwICJmp[c]:= t.patLen;
      t.backJmp[c]:= t.patLen; t.backICJmp[c]:= t.patLen;
    END;
    FOR j:= 0 TO t.patLen - 2 DO
      t.forwJmp[ORD(str[j])]:= t.patLen - j - 1;
      t.forwICJmp[ORD(Util.ToUpper(str[j]))]:= t.patLen - j - 1;
    END;
    FOR j:= t.patLen - 1 TO 1 BY -1 DO
      t.backJmp[ORD(str[j])]:= j;
      t.backICJmp[ORD(Util.ToUpper(str[j]))]:= j;
    END;
  END;
END NewPattern;

PROCEDURE IsWord(t: Text; pos: LONGINT; len: INTEGER): BOOLEAN;

BEGIN
  RETURN ((pos = 0) OR IsDelimeter(t, pos - 1))
         & ((pos + len = t.len) OR IsDelimeter(t, pos + len));
END IsWord;

(* $RangeChk- $OvflChk- $NilChk- *)

(* find next occurance of 'pat', beginning at 'from' *)
PROCEDURE (t: Text) FindNext* (VAR pos: LONGINT; from: LONGINT; flags: SHORTSET): BOOLEAN;

VAR
  getCh: GetChProc;
  jmp: JumpTable;
  pat: K.DynString;
  j, patLen: INTEGER;
  k, len: LONGINT;

BEGIN
  IF t.patLen > 0 THEN
    IF Sett.srIgnoreCase IN flags THEN
      getCh:= BGetCharIC;
      jmp:= t.forwICJmp;
      pat:= t.icPat;
    ELSE
      getCh:= BGetChar;
      jmp:= t.forwJmp;
      pat:= t.pat;
    END;
    len:= t.len;
    patLen:= t.patLen;
    WHILE from + patLen <= len DO
      INC(from, patLen);
      REPEAT
        j:= patLen;
        k:= from;
        REPEAT
          DEC(k);
          DEC(j);
        UNTIL (j < 0) OR (pat[j] # getCh(t, k));
        INC(from, jmp[ORD(getCh(t, from - 1))]);
      UNTIL (j < 0) OR (from > len);
      IF j < 0 THEN  (* found *)
        INC(k);
        IF ~(Sett.srFindWord IN flags) OR IsWord(t, k, patLen) THEN
          pos:= k;
          RETURN TRUE;
        ELSE
          from:= k + 1;
        END;
      END;
    END;
  END;
  RETURN FALSE;
END FindNext;

(* find previous occurance of 'pat', beginning at 'from' *)
PROCEDURE (t: Text) FindPrev* (VAR pos: LONGINT; from: LONGINT; flags: SHORTSET): BOOLEAN;

VAR
  getCh: GetChProc;
  jmp: JumpTable;
  pat: K.DynString;
  j, patLen: INTEGER;
  k, len: LONGINT;

BEGIN
  IF t.patLen > 0 THEN
    IF Sett.srIgnoreCase IN flags THEN
      getCh:= BGetCharIC;
      jmp:= t.backICJmp;
      pat:= t.icPat;
    ELSE
      getCh:= BGetChar;
      jmp:= t.backJmp;
      pat:= t.pat;
    END;
    len:= t.len;
    patLen:= t.patLen;
    WHILE from >= 0 DO
      DEC(from);
      REPEAT
        j:= -1;
        k:= from;
        REPEAT
          INC(k);
          INC(j);
        UNTIL (j >= patLen) OR (pat[j] # getCh(t, k));
        DEC(from, jmp[ORD(getCh(t, from + 1))]);
      UNTIL (j >= patLen) OR (from < -1);
      IF j >= patLen THEN  (* found *)
        DEC(k, patLen);
        IF ~(Sett.srFindWord IN flags) OR IsWord(t, k, patLen) THEN
          pos:= k;
          RETURN TRUE;
        ELSE
          from:= k - 1;
        END;
      END;
    END;
  END;
  RETURN FALSE;
END FindPrev;

(* $RangeChk= $OvflChk= $NilChk= *)

END Texts.