MODULE TextCmds;
(**
	project	= "BlackBox"
	organization	= "www.oberon.ch"
	contributors	= "Oberon microsystems"
	version	= "System/Rsrc/About"
	copyright	= "System/Rsrc/About"
	license	= "Docu/BB-License"
	changes	= "
	- 20151012, center #75, fixig text search with 'Word Begins/Ends With' option 
	- 20170309, center #150, improvements in DevSearch
	- 20170620, center #163, adding decimal tabs to TextRulers
	- 20201003, adimetrius, DoShift to use text attributes (instead of default attributes) when inserting tabs
	"
	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, StdApi;

	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;

		TabDialogHook = POINTER TO RECORD (TextRulers.TabDialogHook) 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;

		tabulator*: RECORD
			ruler: TextRulers.Ruler;
			tabIndex: INTEGER;
			style*: INTEGER;
			radixChar*: ARRAY 2 OF CHAR;
			pos*: ARRAY 7 OF CHAR;
			verticalBar*: BOOLEAN;
		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 RETURN ~Strings.IsIdent(ch)
	END LeftTerminator;

	PROCEDURE RightTerminator (ch: CHAR): BOOLEAN;
	BEGIN RETURN ~Strings.IsIdent(ch)
	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 rd.attr # NIL THEN wr.SetAttr(rd.attr) END;
					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 ResetFindDialog*;
	BEGIN
		find.find := "";
		find.ignoreCase := FALSE;
		find.wordBeginsWith := FALSE;
		find.wordEndsWith := FALSE;
		find.reverseOrientation := FALSE;
		Dialog.Update(find)
	END ResetFindDialog;
	
	PROCEDURE GetFindPattern (c: TextControllers.Controller);
		VAR r: TextModels.Reader; beg, end: INTEGER; i: INTEGER; ch: CHAR;
			new: ARRAY maxPat OF CHAR;
			
		PROCEDURE Equals(x, y: ARRAY OF CHAR; ignoreCase: BOOLEAN): BOOLEAN;
		BEGIN
			IF ignoreCase THEN Strings.ToUpper(x, x); Strings.ToUpper(y, y) END;
			RETURN x = y
		END Equals;
		
	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 # "") & ~Equals(new, find.find, find.ignoreCase) THEN
				ResetFindDialog;
				find.find := new$;
				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;


	(** tabulator dialog **)

	PROCEDURE InitTabDialog;
		VAR ra: TextRulers.Attributes; type: SET; unit: INTEGER;
	BEGIN
		IF (tabulator.ruler # NIL) & (tabulator.tabIndex >=0) THEN
			ra := tabulator.ruler.style.attr;
			type := ra.tabs.tab[tabulator.tabIndex].type;
			IF TextRulers.rightTab IN type THEN
				IF TextRulers.centerTab IN type THEN tabulator.style := 3
				ELSE tabulator.style := 2
				END
			ELSIF TextRulers.centerTab IN type THEN tabulator.style := 1
			ELSE tabulator.style := 0
			END;
			tabulator.radixChar[0] := TextRulers.GetRadixChar(ra, tabulator.tabIndex); tabulator.radixChar[1] := 0X;
			IF Dialog.metricSystem THEN unit := Ports.mm * 10 ELSE unit := Ports.inch END;
			Strings.RealToStringForm(ra.tabs.tab[tabulator.tabIndex].stop / unit, 6, 4, -2, " ", tabulator.pos);
			IF TextRulers.defaultRadixChar = "," THEN 
				tabulator.pos[LEN(tabulator.pos$) - 3] := TextRulers.defaultRadixChar
			END;
			tabulator.verticalBar := TextRulers.barTab IN type;
		END;
		Dialog.Update(tabulator)
	END InitTabDialog;
	
	PROCEDURE CloseTab*;
	BEGIN
		tabulator.ruler := NIL (* avoid pending references to the text *)
	END CloseTab;
	
	PROCEDURE SetTab*;
		VAR p: TextRulers.Prop; ra, a: TextRulers.Attributes;
			pos: ARRAY 7 OF CHAR; val: REAL; unit, res, i, j, stop: INTEGER; ch: CHAR; hasDot: BOOLEAN;
	BEGIN
		IF (tabulator.ruler # NIL) & (tabulator.tabIndex < tabulator.ruler.style.attr.tabs.len) THEN
			ra := tabulator.ruler.style.attr;
			p := ra.Prop()(TextRulers.Prop);
			p.valid := {TextRulers.tabs};
			(* set alignment type *)
			EXCL(p.tabs.tab[tabulator.tabIndex].type, TextRulers.rightTab);
			EXCL(p.tabs.tab[tabulator.tabIndex].type, TextRulers.centerTab);
			CASE tabulator.style OF
			| 0: (* left *)
			| 1: (* center *)
				INCL(p.tabs.tab[tabulator.tabIndex].type, TextRulers.centerTab)
			| 2: (* right *)
				INCL(p.tabs.tab[tabulator.tabIndex].type, TextRulers.rightTab)
			| 3: (* decimal *)
				INCL(p.tabs.tab[tabulator.tabIndex].type, TextRulers.rightTab);
				INCL(p.tabs.tab[tabulator.tabIndex].type, TextRulers.centerTab);
				IF tabulator.radixChar[0] = 0X THEN tabulator.radixChar[0] := TextRulers.defaultRadixChar; 
					Dialog.Update(tabulator)
				END;
				TextRulers.SetRadixCharProp(p, tabulator.tabIndex, tabulator.radixChar[0])
			END;
			(* set tab stop position *)
			j := 0; hasDot := FALSE;
			FOR i := 0 TO LEN(tabulator.pos$) - 1 DO
				ch := tabulator.pos[i];
				IF Strings.IsNumeric(ch) THEN pos[j] := ch; INC(j)
				ELSIF ((ch = ",") OR (ch = ".")) & ~hasDot THEN pos[j] := "."; INC(j); hasDot := TRUE
				END
			END;
			pos[j] := 0X;
			Strings.StringToReal(pos, val, res);
			ASSERT(res = 0);
			IF Dialog.metricSystem THEN unit := Ports.mm * 10 ELSE unit := Ports.inch END;
			val := MIN(val * unit + 0.5, Ports.mm * (1000 * 10));
			stop := SHORT(ENTIER(val));
			IF ((tabulator.tabIndex = 0) OR (p.tabs.tab[tabulator.tabIndex - 1].stop < stop))
			& ((tabulator.tabIndex + 1 >= p.tabs.len) OR (stop < p.tabs.tab[tabulator.tabIndex + 1].stop)) THEN
				p.tabs.tab[tabulator.tabIndex].stop := stop;
			ELSE Dialog.GetOK("#Text:TabErr1", "", "", "", {Dialog.ok}, res)
			END;
			(* set barTab option *)
			IF tabulator.verticalBar THEN INCL(p.tabs.tab[tabulator.tabIndex].type, TextRulers.barTab)
			ELSE EXCL(p.tabs.tab[tabulator.tabIndex].type, TextRulers.barTab)
			END;
			a := TextRulers.ModifiedAttr(ra, p);
			tabulator.ruler.style.SetAttr(a)
		END
	END SetTab;

	PROCEDURE TabGuard* (VAR par: Dialog.Par);
	BEGIN
		par.disabled := (tabulator.ruler = NIL) 
			OR (tabulator.tabIndex < 0) OR (tabulator.tabIndex >=  tabulator.ruler.style.attr.tabs.len)
	END TabGuard;

	PROCEDURE NextTabGuard* (VAR par: Dialog.Par);
	BEGIN
		TabGuard(par);
		par.disabled := par.disabled OR (tabulator.tabIndex + 1 >=  tabulator.ruler.style.attr.tabs.len)
	END NextTabGuard;

	PROCEDURE NextTab*;
	BEGIN
		IF (tabulator.ruler # NIL) & (tabulator.tabIndex + 1 < tabulator.ruler.style.attr.tabs.len) THEN
			INC(tabulator.tabIndex);
			InitTabDialog
		END
	END NextTab;
	
	PROCEDURE PrevTabGuard* (VAR par: Dialog.Par);
	BEGIN
		TabGuard(par);
		par.disabled := par.disabled OR (tabulator.tabIndex = 0)
	END PrevTabGuard;
	
	PROCEDURE PrevTab*;
	BEGIN
		IF (tabulator.ruler # NIL) & (tabulator.tabIndex - 1 >= 0) & (tabulator.ruler.style.attr.tabs.len > 0)  THEN
			DEC(tabulator.tabIndex);
			InitTabDialog
		END
	END PrevTab;

	PROCEDURE DecTabGuard* (VAR par: Dialog.Par);
	BEGIN
		TabGuard(par);
		par.disabled := par.disabled OR (tabulator.style # 3)
	END DecTabGuard;

	PROCEDURE TabUnitGuard* (VAR par: Dialog.Par);
	BEGIN
		TabGuard(par);
		IF Dialog.metricSystem THEN par.label := "cm" ELSE par.label := "inch" END
	END TabUnitGuard;

	PROCEDURE TabNumGuard* (VAR par: Dialog.Par);
		VAR numStr: ARRAY 3 OF CHAR;
	BEGIN
		TabGuard(par);
		IF par.disabled THEN numStr := "" ELSE Strings.IntToString(tabulator.tabIndex + 1, numStr) END;
		Dialog.MapParamString("#Text:TabNum", numStr, "", "", par.label)
	END TabNumGuard;

	PROCEDURE (this: TabDialogHook) Open(ruler: TextRulers.Ruler; tabIndex: INTEGER);
		VAR v: Views.View;
	BEGIN
		tabulator.ruler := ruler; tabulator.tabIndex := tabIndex;
		InitTabDialog;
		StdApi.OpenToolDialog('Text/Rsrc/Tab', '#Text:TabTitle', v)
	END Open;

	PROCEDURE (this: TabDialogHook) Init(ruler: TextRulers.Ruler; tabIndex: INTEGER);
	BEGIN
		IF tabulator.ruler # NIL THEN (* dialog open *)
			tabulator.ruler := ruler; tabulator.tabIndex := tabIndex;
			InitTabDialog
		END
	END Init;

	PROCEDURE (this: TabDialogHook) Refresh(style: TextRulers.Style);
	BEGIN
		IF (tabulator.ruler # NIL) & (tabulator.ruler.style = style) THEN (* dialog open and displaying changed ruler *)
			InitTabDialog
		END
	END Refresh;


	(** 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;

	PROCEDURE NewTabDialogHook(): TabDialogHook;
		VAR h: TabDialogHook;
	BEGIN NEW(h); RETURN h
	END NewTabDialogHook;
	
BEGIN
	TextRulers.tabDialogHook := NewTabDialogHook()
CLOSE
	TextRulers.tabDialogHook := NIL
END TextCmds.
