MODULE TextCmds;
(**

   project   = "BlackBox"
   organization   = "www.oberon.ch"
   contributors   = "Oberon microsystems"
   version   = "System/Rsrc/About"
   copyright   = "System/Rsrc/About"
   license   = "Docu/BB-License"
   changes   = ""
   issues   = ""

**)

(* could eliminate ReplList/ReplOp and use Models.Begin/EndScript instead (as already done for shifting) *)

(* move ListAlienViews to StdCmds and generalize accordingly? *)
   IMPORT


      Strings, Ports, Stores, Models, Views, Controllers, Properties, Dialog, Containers,
      TextModels, TextMappers, TextRulers, TextSetters, TextViews, TextControllers;
   CONST

      (* ShiftOp.left *)
      left = TRUE; right = FALSE;
      (* PreparePat, FindPat *)

      leftTerm = 3X; rightTerm = 4X;
      (* DoReplace mode *)

      replace = 0; replaceAndFind = 1; replaceAll = 2;
      (* FindIn first *)

      first = TRUE; again = FALSE;
      mm = Ports.mm; point = Ports.point; maxPat = 256;

      viewcode = TextModels.viewcode;
      tab = TextModels.tab; line = TextModels.line; para = TextModels.para;
      nbspace = TextModels.nbspace; digitspace = TextModels.digitspace;
      hyphen = TextModels.hyphen;
      nbhyphen = TextModels.nbhyphen; softhyphen = TextModels.softhyphen;
      posKey = "#Text:Position";

      searchAliensKey = "#Text:SearchForAlienViews";   (* dormant code option *)
      alienTypeKey = "#Text:AlienViewType";
      noAliensKey = "#Text:NoAlienViewsFound";
      noRulerKey = "#Text:NoRulerSelected";
      noMatchKey = "#Text:SelectionDoesNotMatch";
      noTargetKey = "#Text:NoTargetFound";
      noSelectionKey = "#Text:NoSelectionFound";
      noPatternKey = "#Text:PatternNotSpecified";
      notFoundKey = "#Text:PatternNotFound";   (* not used *)
      replacingKey = "#System:Replacing";
      shiftingKey = "#Text:Shifting";
      showMarksKey = "#Text:ShowMarks";
      hideMarksKey = "#Text:HideMarks";
      replaceSelectionKey = "#Text:ReplaceAllInSelection";
      replaceAllKey = "#Text:ReplaceAll";
   TYPE


      FindSpec = RECORD
         valid, ignoreCase, wordBeginsWith, wordEndsWith, reverse: BOOLEAN;
         start: INTEGER;
         find: ARRAY maxPat OF CHAR
      END;
      ReplList = POINTER TO RECORD

         next: ReplList;
         beg, end: INTEGER;
         buf: TextModels.Model
      END;
      ReplOp = POINTER TO RECORD (Stores.Operation)

         text: TextModels.Model;
         list, last: ReplList;
         find: FindSpec
      END;
   VAR


      find*: RECORD
         find*: ARRAY maxPat OF CHAR;
         replace*: ARRAY maxPat OF CHAR;
         ignoreCase*, wordBeginsWith*, wordEndsWith*: BOOLEAN;
         reverseOrientation*: BOOLEAN
      END;
      ruler*: RECORD

         pageBreaks*: RECORD
            notInside*, joinPara*: BOOLEAN
         END
      END;
   PROCEDURE Show (t: TextModels.Model; beg, end: INTEGER);


   BEGIN
      TextViews.ShowRange(t, beg, end, TextViews.focusOnly);
      IF beg = end THEN
         TextControllers.SetCaret(t, beg)
      ELSE
         TextControllers.SetSelection(t, beg, end)
      END
   END Show;
   PROCEDURE NoShow (t: TextModels.Model; pos: INTEGER);

   BEGIN
      TextControllers.SetSelection(t, pos, pos);
      TextControllers.SetCaret(t, pos)
   END NoShow;
   PROCEDURE Ruler (): TextRulers.Ruler;

      VAR r: TextRulers.Ruler;
   BEGIN
      r := TextRulers.dir.New(NIL);
      TextRulers.AddTab(r, 4*mm); TextRulers.AddTab(r, 20*mm);
      RETURN r
   END Ruler;
   (* search & replace *)


   PROCEDURE LeftTerminator (ch: CHAR): BOOLEAN;

   BEGIN
      IF ch < 100X THEN
         CASE ch OF
            viewcode, tab, line, para, " ",
            "(", "[", "{", "=",
            hyphen, softhyphen: RETURN TRUE
         ELSE RETURN FALSE
         END
      ELSE RETURN TRUE
      END
   END LeftTerminator;
   PROCEDURE RightTerminator (ch: CHAR): BOOLEAN;

   BEGIN
      IF ch < 100X THEN
         CASE ch OF
            0X, viewcode, tab, line, para, " ",
            "!", "(", ")", ",", ".", ":", ";", "?", "[", "]", "{", "}",
            hyphen, softhyphen: RETURN TRUE
         ELSE RETURN FALSE
         END
      ELSE RETURN TRUE
      END
   END RightTerminator;
   PROCEDURE PreparePat (spec: FindSpec;

                        VAR pat: ARRAY OF CHAR; VAR n: INTEGER;
                        VAR wordBeg, wordEnd: BOOLEAN);
      VAR i: INTEGER; ch: CHAR;
   BEGIN
      i := 0; ch := spec.find[0];
      wordBeg := spec.wordBeginsWith & ~LeftTerminator(ch);
      IF wordBeg THEN pat[0] := leftTerm; n := 1 ELSE n := 0 END;
      WHILE ch # 0X DO
         IF ch # softhyphen THEN
            IF spec.ignoreCase THEN pat[n] := Strings.Upper(ch) ELSE pat[n] := ch END;
            INC(n)
         END;
         INC(i); ch := spec.find[i]
      END;
      wordEnd := spec.wordEndsWith & ~RightTerminator(pat[n - 1]);
      IF wordEnd THEN pat[n] := rightTerm; INC(n) END
   END PreparePat;
   PROCEDURE FindPat (t: TextModels.Model; spec: FindSpec; VAR beg, end: INTEGER);

   (* post: beg < end => t[beg, end) = spec.find, start <= beg; else beg = end *)
      VAR r: TextModels.Reader; start: INTEGER;
         i, j, b, e, n: INTEGER; ch0, ch, ch1: CHAR; wordBeg, wordEnd, icase: BOOLEAN;
         pat, ref: ARRAY maxPat OF CHAR;   (* ref [b..e) is readback buffer *)
         pos0, pos1, absStart: INTEGER;
         orientation: INTEGER;
   BEGIN
      IF spec.reverse THEN
         orientation := -1; absStart := t.Length();
         PreparePat(spec, ref, n, wordEnd, wordBeg);
         i := n; j := 0; REPEAT DEC(i); pat[j] := ref[i]; INC(j) UNTIL i = 0   (* Just reverse the pattern... *)
      ELSE
         orientation := 1; absStart := 0;
         PreparePat(spec, pat, n, wordBeg, wordEnd)
      END;
      start := spec.start; icase := spec.ignoreCase;
      r := t.NewReader(NIL); i := 0;
      IF wordBeg THEN
         IF start # absStart THEN
            DEC(start, orientation)
         ELSE
            r.SetPos(absStart);
            IF spec.reverse THEN r.ReadPrevChar(ch) ELSE r.ReadChar(ch) END;
            IF ~LeftTerminator(ch) THEN i := 1 END
         END
      END;
      r.SetPos(start); IF spec.reverse THEN r.ReadPrevChar(ch) ELSE r.ReadChar(ch) END;
      pos0 := start; pos1 := start;
      IF icase THEN ch := Strings.Upper(ch) END;
      ref[0] := ch; ch0 := ch; j := 0; b := 0; e := 1;
      WHILE ~r.eot & (i < n) DO
         ch1 := pat[i];
         IF (ch1 = ch)
         OR (ch1 = leftTerm) & LeftTerminator(ch)
         OR (ch1 = rightTerm) & RightTerminator(ch) THEN
            INC(i); j := (j + 1) MOD maxPat
         ELSIF ch = softhyphen THEN
            j := (j + 1) MOD maxPat
         ELSE
            i := 0; INC(pos0, orientation); b := (b + 1) MOD maxPat; j := b
         END;
         IF j # e THEN
            ch := ref[j]
         ELSE
            INC(pos1, orientation);
            IF spec.reverse THEN r.ReadPrevChar(ch) ELSE r.ReadChar(ch) END;
            IF icase THEN ch := Strings.Upper(ch) END;
            ref[j] := ch; e := (e + 1) MOD maxPat
         END
      END;
      IF wordEnd & ~((i + 1 = n) & r.eot) THEN DEC(pos1, orientation) END;
      IF (n > 0) & ((i = n) OR wordEnd & (i + 1 = n) & r.eot) THEN
         IF wordBeg & ((pos0 # absStart) OR LeftTerminator(ch0)) THEN INC(pos0, orientation) END
      ELSE
         pos0 := pos1
      END;
      IF spec.reverse THEN
         beg := pos1; end := pos0
      ELSE
         beg := pos0; end := pos1
      END
   END FindPat;
   PROCEDURE OverrideSpecWithOption (VAR spec: FindSpec; option: ARRAY OF CHAR);

      VAR i: INTEGER; choice: BOOLEAN; ch: CHAR;
   BEGIN
      choice := TRUE; i := 0; ch := option[i];
      WHILE ch # 0X DO
         CASE option[i] OF
            '~': choice := ~choice
         |'I', 'i': spec.ignoreCase := choice; choice := TRUE
         |'B', 'b': spec.wordBeginsWith := choice; choice := TRUE
         |'E', 'e': spec.wordEndsWith := choice; choice := TRUE
         |'R', 'r': spec.reverse := choice; choice := TRUE
         ELSE choice := TRUE
         END;
         INC(i); ch := option[i]
      END
   END OverrideSpecWithOption;
   PROCEDURE SetSpec (VAR spec: FindSpec; pos0, pos1: INTEGER; option: ARRAY OF CHAR);

   BEGIN
      ASSERT(find.find # "", 20);
      spec.valid := TRUE;
      spec.ignoreCase := find.ignoreCase;
      spec.wordBeginsWith := find.wordBeginsWith;
      spec.wordEndsWith := find.wordEndsWith;
      spec.reverse := find.reverseOrientation;
      OverrideSpecWithOption(spec, option);
      IF spec.reverse THEN spec.start := pos1
      ELSE spec.start := pos0
      END;
      spec.find := find.find$
   END SetSpec;
   PROCEDURE SetFindSpec (c: TextControllers.Controller; first: BOOLEAN; option: ARRAY OF CHAR;

      VAR spec: FindSpec
   );
      VAR (*start,*) pos0, pos1, beg, end: INTEGER;
   BEGIN
      IF first THEN pos0 := 0; pos1 := pos0
      ELSIF c.HasCaret() THEN pos0 := c.CaretPos(); pos1 := pos0
      ELSIF c.HasSelection() THEN c.GetSelection(beg, end); pos0 := beg + 1; pos1 := end - 1
      ELSE pos0 := 0; pos1 := pos0
      END;
      SetSpec(spec, pos0, pos1, option);
      IF spec.reverse THEN
         IF spec.start = 0 THEN spec.start := c.text.Length() END
      ELSE
         IF spec.start = c.text.Length() THEN spec.start := 0 END
      END
   END SetFindSpec;
   PROCEDURE ReplBuf (target: TextModels.Model; pos: INTEGER): TextModels.Model;


      VAR buf: TextModels.Model; attr: TextModels.Attributes; rd: TextModels.Reader;
         out: TextModels.Writer; i: INTEGER;
   BEGIN
      rd := target.NewReader(NIL); rd.SetPos(pos); rd.ReadRun(attr);
      buf := TextModels.CloneOf(target); out := buf.NewWriter(NIL); out.SetPos(0);
      IF attr # NIL THEN out.SetAttr(attr) END;
      i := 0; WHILE find.replace[i] # 0X DO out.WriteChar(find.replace[i]); INC(i) END;
      RETURN buf
   END ReplBuf;
   (* operations *)


   PROCEDURE (op: ReplOp) Do;

      VAR u, v: ReplList; text, save: TextModels.Model; beg, end, delta, len: INTEGER;
   BEGIN
      text := op.text;
      u := op.list; v := NIL; delta := 0;
      WHILE u # NIL DO
         INC(u.beg, delta); INC(u.end, delta);
         IF u.end > u.beg THEN
            save := TextModels.CloneOf(text); save.Insert(0, text, u.beg, u.end);
            DEC(delta, u.end - u.beg)
         ELSE
            save := NIL
         END;
         IF u.buf # NIL THEN
            len := u.buf.Length();
            text.Insert(u.beg, u.buf, 0, len);
            u.end := u.beg + len;
            INC(delta, len)
         ELSE
            u.end := u.beg
         END;
         u.buf := save;
         v := u; u := u.next
      END;
      IF op.find.valid THEN
         FindPat(text, op.find, beg, end); op.find.valid := FALSE;
         IF beg = end THEN Dialog.Beep END
      ELSIF v # NIL THEN
         beg := v.beg; end := v.end
      ELSE
         beg := 0; end := 0
      END;
      IF end > beg THEN Show(text, beg, end) ELSE NoShow(text, beg) END
   END Do;
   PROCEDURE AddRepl (op: ReplOp; beg, end: INTEGER; reverse: BOOLEAN);

      VAR u: ReplList;
   BEGIN
      NEW(u); u.beg := beg; u.end := end; u.buf := ReplBuf(op.text, beg);
      IF reverse THEN   (* append *)
         u.next := op.list; op.list := u
      ELSE   (* prepend *)
         IF op.list = NIL THEN op.list := u ELSE op.last.next := u END;
         op.last := u
      END
   END AddRepl;
   PROCEDURE DoReplaceThis (

      t: TextModels.Model; mode: INTEGER;
      firstBeg, firstEnd: INTEGER;
      rngBeg, rngEnd: INTEGER;
      option: ARRAY OF CHAR
   );
      VAR op: ReplOp; spec: FindSpec; beg, end, len: INTEGER;
   BEGIN
      NEW(op); op.text := t; op.list := NIL;
      beg := firstBeg; end := firstEnd;
      IF mode IN {replace, replaceAndFind} THEN
         AddRepl(op, firstBeg, firstEnd, spec.reverse)
      END;
      IF mode = replaceAndFind THEN
         SetSpec(op.find, firstBeg + (* LEN(find.replace$) *) ReplBuf(t, 0).Length(), firstBeg, option)
      ELSE
         op.find.valid := FALSE
      END;
      IF mode = replaceAll THEN
         len := LEN(find.find$);
         SetSpec(spec, 0, t.Length(), option);
         WHILE (rngBeg <= beg) & (beg < end) & (end <= rngEnd)DO
            AddRepl(op, beg, end, spec.reverse);
            IF spec.reverse THEN spec.start := beg ELSE spec.start := beg + len END;
            FindPat(t, spec, beg, end)
         END
      END;
      Models.Do(t, replacingKey, op)
   END DoReplaceThis;
   PROCEDURE DoReplace (c: TextControllers.Controller; mode: INTEGER; option: ARRAY OF CHAR);

      VAR t: TextModels.Model; spec: FindSpec;
         selBeg, selEnd, beg, end, len0: INTEGER; hasSel0: BOOLEAN;
   BEGIN
      IF c # NIL THEN
         t := c.text; len0 := t.Length(); hasSel0 := c.HasSelection();
         IF hasSel0 THEN
            c.GetSelection(selBeg, selEnd);
            IF selEnd < len0 THEN
               SetSpec(spec, selBeg, selEnd + 1, option)
            ELSE SetSpec(spec, selBeg, selEnd, option)
            END
         ELSE
            selBeg := 0; selEnd := len0;
            SetFindSpec(c, (* again *) mode = replaceAll, option, spec)
         END;
         FindPat(t, spec, beg, end);
         IF mode = replaceAll THEN
            IF (selBeg <= beg) & (beg < end) & (end <= selEnd) THEN
               DoReplaceThis(t, mode, beg, end, selBeg, selEnd, option);
               IF hasSel0 THEN Show(c.text, selBeg, selEnd + t.Length() - len0) END
            ELSE NoShow(c.text, 0); Dialog.Beep
            END
         ELSIF hasSel0 THEN
            IF (beg = selBeg) & (end = selEnd) THEN
               DoReplaceThis(t, mode, selBeg, selEnd, 0, len0, option)
            ELSE Dialog.ShowParamMsg(noMatchKey, spec.find, "", "")
            END
         ELSE Dialog.ShowMsg(noSelectionKey)
         END
      ELSE Dialog.ShowMsg(noTargetKey)
      END
   END DoReplace;
   PROCEDURE DoShift (c: TextControllers.Controller; left: BOOLEAN);

      VAR script: Stores.Operation;
         t: TextModels.Model; st: TextSetters.Setter;
         rd: TextModels.Reader; wr: TextModels.Writer;
         box: TextSetters.LineBox; beg, pos, end: INTEGER; ch: CHAR;
   BEGIN
      IF (c # NIL) & (c.HasSelection() OR c.HasCaret()) THEN
         t := c.text;
         IF c.HasSelection() THEN c.GetSelection(beg, end) ELSE beg := c.CaretPos(); end := beg END;
         st := c.view.ThisSetter(); beg := st.ThisSequence(beg); pos := beg;
         rd := t.NewReader(NIL); rd.SetPos(pos);
         IF ~left THEN wr := t.NewWriter(NIL) END;
         Models.BeginScript(t, shiftingKey, script);
         REPEAT
            rd.ReadChar(ch);
            IF rd.view # NIL THEN
               st.GetLine(pos, box);
               IF box.rbox THEN ch := para END
            END;
            IF left & ((ch = tab) OR (ch = " ") OR (ch = digitspace) OR (ch = nbspace)) THEN
               t.Delete(pos, pos + 1); rd.SetPos(pos); DEC(end)
            ELSIF ~left & (ch # line) & (ch # para) THEN
               wr.SetPos(pos);
               IF (ch = " ") OR (ch = digitspace) OR (ch = nbspace) THEN
                  wr.WriteChar(ch)
               ELSE wr.WriteChar(tab)
               END;
               INC(pos); INC(end)
            ELSE INC(pos)
            END;
            WHILE ~rd.eot & (ch # line) & (ch # para) DO
               INC(pos); rd.ReadChar(ch)
            END
         UNTIL rd.eot OR (pos >= end);
         Models.EndScript(t, script);
         IF end > beg THEN TextControllers.SetSelection(t, beg, end) END
      END
   END DoShift;
   (** commands **)

   PROCEDURE ListAlienViews*;

      VAR t: TextModels.Model; v: TextViews.View; wr: TextMappers.Formatter;
         rd: TextModels.Reader; view: Views.View;
         type: Stores.TypeName; none: BOOLEAN;
   BEGIN
      t := TextViews.FocusText();
      IF t # NIL THEN
         wr.ConnectTo(TextModels.dir.New());
         rd := t.NewReader(NIL); rd.ReadView(view); none := TRUE;
         WHILE view # NIL DO
            IF view IS Views.Alien THEN
               IF none THEN
                  wr.WriteTab; wr.WriteMsg(posKey);
                  wr.WriteTab; wr.WriteMsg(alienTypeKey); wr.WriteLn
               END;
               none := FALSE;
               type := view(Views.Alien).store.path[0]$;
               wr.WriteTab;
               wr.WriteIntForm(rd.Pos() - 1,
                  TextMappers.decimal, 5, nbspace, TextMappers.hideBase);
               wr.WriteTab; wr.WriteString(type); wr.WriteLn
            END;
            rd.ReadView(view)
         END;
         IF none THEN wr.WriteString(noAliensKey); wr.WriteLn END;
         v := TextViews.dir.New(wr.rider.Base());
         v.SetDefaults(Ruler(), TextViews.dir.defAttr);
         Views.OpenView(v)
      END
   END ListAlienViews;
   PROCEDURE ToggleMarksGuard* (VAR par: Dialog.Par);


      VAR v: TextViews.View;
   BEGIN
      v := TextViews.Focus();
      IF (v # NIL) & v.HidesMarks() THEN par.label := showMarksKey
      ELSE par.label := hideMarksKey
      END
   END ToggleMarksGuard;
   PROCEDURE ToggleMarks*;

      VAR v: TextViews.View;
   BEGIN
      v := TextViews.Focus();
      IF v # NIL THEN v.DisplayMarks(~v.HidesMarks()) END
   END ToggleMarks;
   PROCEDURE ShowMarks*;

      VAR v: TextViews.View;
   BEGIN
      v := TextViews.Focus();
      IF (v # NIL) & v.HidesMarks() THEN v.DisplayMarks(TextViews.show) END
   END ShowMarks;
   PROCEDURE HideMarks*;

      VAR v: TextViews.View;
   BEGIN
      v := TextViews.Focus();
      IF (v # NIL) & ~v.HidesMarks() THEN v.DisplayMarks(TextViews.hide) END
   END HideMarks;
   PROCEDURE MakeDefaultRulerGuard* (VAR par: Dialog.Par);

      VAR c: TextControllers.Controller; v: Views.View;
   BEGIN
      c := TextControllers.Focus();
      IF c # NIL THEN
         v := c.Singleton();
         IF (v = NIL) OR ~(v IS TextRulers.Ruler) THEN par.disabled := TRUE END
      ELSE par.disabled := TRUE
      END
   END MakeDefaultRulerGuard;
   PROCEDURE MakeDefaultRuler*;

      VAR c: TextControllers.Controller; rd: TextModels.Reader;
         r: TextRulers.Ruler; a: TextModels.Attributes;
         beg, end: INTEGER;
   BEGIN
      c := TextControllers.Focus();
      IF c # NIL THEN
         IF c.HasSelection() THEN
            c.GetSelection(beg, end);
            rd := c.text.NewReader(NIL); rd.SetPos(beg); rd.Read;
            IF (rd.view # NIL) & (rd.view IS TextRulers.Ruler) THEN
               c.view.PollDefaults(r, a);
               c.view.SetDefaults(rd.view(TextRulers.Ruler), a)
            ELSE Dialog.ShowMsg(noRulerKey)
            END
         ELSE Dialog.ShowMsg(noSelectionKey)
         END
      ELSE Dialog.ShowMsg(noTargetKey)
      END
   END MakeDefaultRuler;
   PROCEDURE MakeDefaultAttributes*;

      VAR c: TextControllers.Controller; rd: TextModels.Reader;
         r: TextRulers.Ruler; a: TextModels.Attributes;
         beg, end: INTEGER;
   BEGIN
      c := TextControllers.Focus();
      IF c # NIL THEN
         IF c.HasSelection() THEN
            c.GetSelection(beg, end);
            rd := c.text.NewReader(NIL); rd.SetPos(beg); rd.Read;
            c.view.PollDefaults(r, a);
            c.view.SetDefaults(r, rd.attr)
         ELSE Dialog.ShowMsg(noSelectionKey)
         END
      ELSE Dialog.ShowMsg(noTargetKey)
      END
   END MakeDefaultAttributes;
   PROCEDURE ShiftLeft*;

   BEGIN
      DoShift(TextControllers.Focus(), left)
   END ShiftLeft;
   PROCEDURE ShiftRight*;

   BEGIN
      DoShift(TextControllers.Focus(), right)
   END ShiftRight;
   PROCEDURE Subscript*;


      VAR q, p0: Properties.Property; p: TextModels.Prop;
   BEGIN
      Properties.CollectProp(q);
      p0 := q; WHILE (p0 # NIL) & ~(p0 IS TextModels.Prop) DO p0 := p0.next END;
      NEW(p); p.valid := {TextModels.offset};
      IF (p0 # NIL) & (TextModels.offset IN p0.valid) THEN
         p.offset := p0(TextModels.Prop).offset - point
      ELSE p.offset := -point
      END;
      Properties.EmitProp(NIL, p)
   END Subscript;
   PROCEDURE Superscript*;

      VAR q, p0: Properties.Property; p: TextModels.Prop;
   BEGIN
      Properties.CollectProp(q);
      p0 := q; WHILE (p0 # NIL) & ~(p0 IS TextModels.Prop) DO p0 := p0.next END;
      NEW(p); p.valid := {TextModels.offset};
      IF (p0 # NIL) & (TextModels.offset IN p0.valid) THEN
         p.offset := p0(TextModels.Prop).offset + point
      ELSE p.offset := point
      END;
      Properties.EmitProp(NIL, p)
   END Superscript;
   PROCEDURE ForceToNewLine (c: TextControllers.Controller);


      VAR st: TextSetters.Setter; pos, start: INTEGER; msg: Controllers.EditMsg;
   BEGIN
      IF c.HasCaret() THEN
         pos := c.CaretPos();
         st := c.view.ThisSetter(); start := st.ThisLine(pos);
         IF pos # start THEN
            msg.op := Controllers.pasteChar; msg.char := line;
            Controllers.Forward(msg)
         END
      END
   END ForceToNewLine;
   PROCEDURE InsertParagraph*;

      VAR c: TextControllers.Controller; script: Stores.Operation; msg: Controllers.EditMsg;
   BEGIN
      c := TextControllers.Focus();
      IF c # NIL THEN
         Models.BeginScript(c.text, "#Text:InsertParagraph", script);
         ForceToNewLine(c);
         msg.op := Controllers.pasteChar; msg.char := para;
         Controllers.Forward(msg);
         Models.EndScript(c.text, script)
      END
   END InsertParagraph;
   PROCEDURE InsertRuler*;

      VAR c: TextControllers.Controller; script: Stores.Operation;
         rd: TextModels.Reader; r: TextRulers.Ruler;
         pos, end: INTEGER;
   BEGIN
      c := TextControllers.Focus();
      IF c # NIL THEN
         r := NIL;
         IF c.HasSelection() THEN
            c.GetSelection(pos, end);
            rd := c.text.NewReader(NIL); rd.SetPos(pos); rd.Read;
            IF (rd.view # NIL) & (rd.view IS TextRulers.Ruler) THEN
               r := rd.view(TextRulers.Ruler)
            END
         ELSE pos := c.CaretPos()
         END;
         IF r = NIL THEN r := TextViews.ThisRuler(c.view, pos) END;
         r := TextRulers.CopyOf(r, Views.deep);
         Models.BeginScript(c.text, "#Text:InsertRuler", script);
         ForceToNewLine(c);
         c.view.DisplayMarks(TextViews.show);
         Controllers.PasteView(r, Views.undefined, Views.undefined, FALSE);
         Models.EndScript(c.text, script)
      END
   END InsertRuler;
   PROCEDURE InsertSoftHyphen*;

      VAR msg: Controllers.EditMsg;
   BEGIN
      msg.op := Controllers.pasteChar; msg.char := softhyphen;
      Controllers.Forward(msg)
   END InsertSoftHyphen;
   PROCEDURE InsertNBHyphen*;

      VAR msg: Controllers.EditMsg;
   BEGIN
      msg.op := Controllers.pasteChar; msg.char := nbhyphen;
      Controllers.Forward(msg)
   END InsertNBHyphen;
   PROCEDURE InsertNBSpace*;

      VAR msg: Controllers.EditMsg;
   BEGIN
      msg.op := Controllers.pasteChar; msg.char := nbspace;
      Controllers.Forward(msg)
   END InsertNBSpace;
   PROCEDURE InsertDigitSpace*;

      VAR msg: Controllers.EditMsg;
   BEGIN
      msg.op := Controllers.pasteChar; msg.char := digitspace;
      Controllers.Forward(msg)
   END InsertDigitSpace;
   PROCEDURE GetFindPattern (c: TextControllers.Controller);


      VAR r: TextModels.Reader; beg, end: INTEGER; i: INTEGER; ch: CHAR;
         new: ARRAY maxPat OF CHAR;
   BEGIN
      IF (c # NIL) & c.HasSelection() THEN
         c.GetSelection(beg, end);
         r := c.text.NewReader(NIL); r.SetPos(beg); r.ReadChar(ch); i := 0;
         WHILE (r.Pos() <= end) & (i < maxPat - 1) DO
            new[i] := ch; INC(i); r.ReadChar(ch)
         END;
         new[i] := 0X;
         IF (new # "") & (new # find.find) THEN
            find.find := new$;
            find.ignoreCase := FALSE;
            find.wordBeginsWith := FALSE; find.wordEndsWith := FALSE;
            Dialog.Update(find)
         END
      END
   END GetFindPattern;
   PROCEDURE FindIn (c: TextControllers.Controller; first: BOOLEAN; option: ARRAY OF CHAR);

      VAR spec: FindSpec; beg, end: INTEGER;
   BEGIN
      IF c # NIL THEN
         IF find.find # "" THEN
            SetFindSpec(c, first, option, spec);
            FindPat(c.text, spec, beg, end);
            IF end > beg THEN Show(c.text, beg, end) ELSE NoShow(c.text, 0); Dialog.Beep END
         ELSE Dialog.ShowMsg(noPatternKey)
         END
      ELSE Dialog.ShowMsg(noTargetKey)
      END
   END FindIn;
   PROCEDURE FindGuard* (VAR par: Dialog.Par);


      VAR c: TextControllers.Controller;
   BEGIN
      c := TextControllers.Focus();
      IF (c = NIL) OR (find.find = "") THEN par.disabled := TRUE END
   END FindGuard;
   PROCEDURE FindFirst* (option: ARRAY OF CHAR);

   BEGIN
      FindIn(TextControllers.Focus(), first, option)
   END FindFirst;
   PROCEDURE FindAgainGuard* (VAR par: Dialog.Par);

      VAR c: TextControllers.Controller;
   BEGIN
      c := TextControllers.Focus();
      IF (c = NIL) OR (~c.HasSelection() & (find.find = "")) THEN par.disabled := TRUE END
   END FindAgainGuard;
   PROCEDURE FindAgain* (option: ARRAY OF CHAR);

   BEGIN
      FindIn(TextControllers.Focus(), again, option)
   END FindAgain;
   PROCEDURE ReplaceGuard* (VAR par: Dialog.Par);


      VAR c: TextControllers.Controller;
   BEGIN
      c := TextControllers.Focus();
      IF (c = NIL) OR (Containers.noCaret IN c.opts) OR ~c.HasSelection() OR (find.find = "") THEN
         par.disabled := TRUE
      END
   END ReplaceGuard;
   PROCEDURE Replace* (option: ARRAY OF CHAR);

   BEGIN
      DoReplace(TextControllers.Focus(), replace, option)
   END Replace;
   PROCEDURE ReplaceAndFindNext* (option: ARRAY OF CHAR);

   BEGIN
      DoReplace(TextControllers.Focus(), replaceAndFind, option)
   END ReplaceAndFindNext;
   PROCEDURE ReplaceAllGuard* (VAR par: Dialog.Par);


      VAR c: TextControllers.Controller;
   BEGIN
      c := TextControllers.Focus();
      IF (c = NIL) OR (Containers.noCaret IN c.opts) OR (find.find = "") THEN
         par.disabled := TRUE
      ELSE
         IF c.HasSelection() THEN par.label := replaceSelectionKey ELSE par.label := replaceAllKey END
      END
   END ReplaceAllGuard;
   PROCEDURE ReplaceAll* (option: ARRAY OF CHAR);

   BEGIN
      DoReplace(TextControllers.Focus(), replaceAll, option)
   END ReplaceAll;
   PROCEDURE SetNormalOrientation*;


   BEGIN
      find.reverseOrientation := FALSE;
      Dialog.Update(find)
   END SetNormalOrientation;
   PROCEDURE SetReverseOrientation*;

   BEGIN
      find.reverseOrientation := TRUE;
      Dialog.Update(find)
   END SetReverseOrientation;
   PROCEDURE InitFindDialog*;

   BEGIN
      GetFindPattern(TextControllers.Focus())
   END InitFindDialog;
   (** ruler dialog **)


   PROCEDURE InitRulerDialog*;

      VAR v: Views.View; ra: TextRulers.Attributes;
   BEGIN
      v := Controllers.FocusView();
      IF v # NIL THEN
         WITH v: TextRulers.Ruler DO
            ra := v.style.attr;
            ruler.pageBreaks.notInside := TextRulers.noBreakInside IN ra.opts;
            ruler.pageBreaks.joinPara := TextRulers.parJoin IN ra.opts
         ELSE
         END
      END
   END InitRulerDialog;
   PROCEDURE SetRuler*;

      VAR v: Views.View; p: TextRulers.Prop;
   BEGIN
      v := Controllers.FocusView();
      IF v # NIL THEN
         WITH v: TextRulers.Ruler DO
            NEW(p); p.valid := {TextRulers.opts};
            p.opts.mask := {TextRulers.noBreakInside, TextRulers.parJoin};
            p.opts.val := {};
            IF ruler.pageBreaks.notInside THEN INCL(p.opts.val, TextRulers.noBreakInside) END;
            IF ruler.pageBreaks.joinPara THEN INCL(p.opts.val, TextRulers.parJoin) END;
            Properties.EmitProp(NIL, p)
         ELSE
         END
      END
   END SetRuler;
   (** standard text-related guards **)


   PROCEDURE FocusGuard* (VAR par: Dialog.Par);

   (** in non-TextView menus; otherwise implied by menu type **)
   BEGIN
      IF TextViews.Focus() = NIL THEN par.disabled := TRUE END
   END FocusGuard;
   PROCEDURE EditGuard* (VAR par: Dialog.Par);

   (** in non-TextView menus; otherwise use "StdCmds.EditGuard" **)
      VAR c: TextControllers.Controller;
   BEGIN
      c := TextControllers.Focus();
      IF (c = NIL) OR (Containers.noCaret IN c.opts) THEN par.disabled := TRUE END
   END EditGuard;
   PROCEDURE SelectionGuard* (VAR par: Dialog.Par);

   (** in non-TextView menus; otherwise use "StdCmds.SelectionGuard" **)
      VAR c: TextControllers.Controller;
   BEGIN
      c := TextControllers.Focus();
      IF (c = NIL) OR ~c.HasSelection() THEN par.disabled := TRUE END
   END SelectionGuard;
   PROCEDURE EditSelectionGuard* (VAR par: Dialog.Par);

   (** in non-TextView menus; otherwise use "StdCmds.SelectionGuard" **)
      VAR c: TextControllers.Controller;
   BEGIN
      c := TextControllers.Focus();
      IF (c = NIL) OR (Containers.noCaret IN c.opts) OR ~c.HasSelection() THEN par.disabled := TRUE END
   END EditSelectionGuard;
   PROCEDURE SingletonGuard* (VAR par: Dialog.Par);

   (** in non-TextView menus; otherwise use "StdCmds.SingletonGuard" **)
      VAR c: TextControllers.Controller;
   BEGIN
      c := TextControllers.Focus();
      IF (c = NIL) OR (c.Singleton() = NIL) THEN par.disabled := TRUE END
   END SingletonGuard;
END TextCmds.