TempTextEditImpl.mesa
Copyright © 1985, 1986 by Xerox Corporation. All rights reserved.
written by Bill Paxton, March 1981
last edit by Bill Paxton, December 28, 1982 11:43 am
Last Edited by: Maxwell, January 5, 1983 3:31 pm
Michael Plass, April 1, 1985 4:14:29 pm PST
Doug Wyatt, September 9, 1986 5:18:44 pm PDT
DIRECTORY
Atom USING [DottedPairNode, GetPName, PropList],
CheckNode USING [CheckRope, CheckRuns],
EditNotify USING [Change, Notify],
NodeAddrs USING [MapTextAddrs, MoveTextAddr, PutTextAddr, Replace, UnpinAll],
NodeProps USING [GetProp, PutProp],
Rope USING [Balance, Cat, Concat, Fetch, Flatten, FromChar, FromProc, FromRefText, InlineSize, Replace, ROPE, Size, Substr],
RopeEdit USING [AlphaNumericChar, LowerCase, ReplaceByTEXT, UpperCase],
RopeReader USING [FreeRopeReader, Get, GetRope, GetRopeReader, Ref, SetPosition],
Rosary USING [CatSegments, Fetch, FromItem, FromProcProc, MapRuns, ROSARY, RunActionType, Size],
TextEdit USING [CapChange, CharSet, end, MapPropsAction, ModifyPropsAction, String, Text],
TextLooks USING [allLooks, ChangeLooks, Concat, CreateRun, FetchLooks, Flatten, Looks, LooksStats, noLooks, Replace, ReplaceByRun, Runs, Substr],
TextNode USING [countMax, Location, NewTextNode, Node, Root],
UndoEvent USING [Event, Note, SubEvent];
TextEditImpl: CEDAR MONITOR
IMPORTS Atom, CheckNode, EditNotify, NodeAddrs, NodeProps, Rope, RopeEdit, RopeReader, Rosary, TextEdit, TextLooks, TextNode, UndoEvent
EXPORTS TextEdit, UndoEvent =
BEGIN
Change: PUBLIC TYPE = EditNotify.Change;
Node: TYPE = TextNode.Node;
ROPE: TYPE = Rope.ROPE;
ROSARY: TYPE = Rosary.ROSARY;
Runs: TYPE = TextLooks.Runs;
Looks: TYPE = TextLooks.Looks;
noLooks: Looks = TextLooks.noLooks;
allLooks: Looks = TextLooks.allLooks;
Event: TYPE = UndoEvent.Event;
CapChange: TYPE ~ TextEdit.CapChange;
MapPropsAction: TYPE ~ TextEdit.MapPropsAction;
ModifyPropsAction: TYPE ~ TextEdit.ModifyPropsAction;
CharSet: TYPE ~ TextEdit.CharSet;
Location: TYPE ~ TextNode.Location;
Text: TYPE ~ TextEdit.Text;
String: TYPE ~ TextEdit.String;
maxLen: INT ~ INT.LAST;
Assertion: TYPE ~ BOOL[TRUE..TRUE];
Cache of notify records
scratch1, scratch2, scratch3: REF Change.ChangingText ← NIL;
Alloc: ENTRY PROC RETURNS [scratch: REF Change.ChangingText] = {
ENABLE UNWIND => NULL;
IF scratch3 # NIL THEN { scratch ← scratch3; scratch3 ← NIL }
ELSE IF scratch2 # NIL THEN { scratch ← scratch2; scratch2 ← NIL }
ELSE IF scratch1 # NIL THEN { scratch ← scratch1; scratch1 ← NIL }
ELSE scratch ← NEW[Change.ChangingText];
};
Free: ENTRY PROC [scratch: REF Change.ChangingText] = {
ENABLE UNWIND => NULL;
IF scratch3 = scratch OR scratch2 = scratch OR scratch1 = scratch THEN ERROR;
IF scratch3 = NIL THEN scratch3 ← scratch
ELSE IF scratch2 = NIL THEN scratch2 ← scratch
ELSE IF scratch1 = NIL THEN scratch1 ← scratch;
};
Fetch info operations
Size: PUBLIC PROC [node: Node] RETURNS [INT] =
{ RETURN [IF node=NIL THEN 0 ELSE Rope.InlineSize[node.rope]] };
GetRope: PUBLIC PROC [text: Node] RETURNS [ROPE] =
{ RETURN [IF node=NIL THEN NIL ELSE node.rope] };
GetRuns: PUBLIC PROC [node: Node] RETURNS [Runs] =
{ RETURN [IF node=NIL THEN NIL ELSE node.runs] };
GetRosary: PROC [node: Node, name: ATOM] RETURNS [ROSARY] ~ {
WITH NodeProps.GetProp[node, name] SELECT FROM
rosary: ROSARY => RETURN[rosary];
ENDCASE => RETURN[NIL];
};
GetCharSets: PROC [node: Node] RETURNS [ROSARY] ~ INLINE {
RETURN[IF node.hascharsets THEN GetRosary[node, $CharSets] ELSE NIL];
};
GetCharProps: PROC [node: Node] RETURNS [ROSARY] ~ INLINE {
RETURN[IF node.hascharprops THEN GetRosary[node, $CharProps] ELSE NIL];
};
Fetch: PUBLIC PROC [text: Node, index: INT] RETURNS [charSet: CharSet, char: CHAR, looks: Looks] = {
[charSet, char] ← FetchChar[text, index];
looks ← FetchLooks[text, index];
};
FetchChar: PUBLIC PROC [text: Node, index: INT] RETURNS [charSet: CharSet ← 0, char: CHAR] = {
charSets: ROSARY ~ GetCharSets[text];
IF charSets#NIL THEN WITH Rosary.Fetch[charSets, index] SELECT FROM
refCharSet: REF CharSet => charSet ← refCharSet^;
ENDCASE;
char ← Rope.Fetch[text.rope, index];
};
FetchLooks: PUBLIC PROC [text: Node, index: INT] RETURNS [Looks] = {
-- returns the looks for the character at the given location
-- use reader's if getting looks for sequence of locations
RETURN [TextLooks.FetchLooks[text.runs, index]];
};
Text Editing Operations
ClipLocation: PROC [loc: Location, size: INT] RETURNS [Location] ~ INLINE {
loc.where ← MIN[MAX[0, loc.where], size];
RETURN[loc];
};
ClipText: PROC [text: Text, size: INT] RETURNS [Text] ~ INLINE {
text.start ← MIN[MAX[0, text.start], size];
text.len ← MIN[MAX[0, text.len], size-text.start];
RETURN[text];
};
ClipString: PROC [string: String] RETURNS [String] ~ INLINE {
size: NAT ~ string.text.length;
string.start ← MIN[string.start, size];
string.len ← MIN[string.len, size-string.start];
RETURN[string];
};
rosaryOfNil: ROSARY ~ Rosary.FromItem[NIL, maxLen];
RosaryReplace: PROC [dest: ROSARY, destSize: INT, destStart: INT, destLen: INT, source: ROSARY, sourceStart: INT, sourceLen: INT] RETURNS [rosary: ROSARY] ~ {
notNil: Rosary.RunActionType ~ { quit ← item#NIL };
d: ROSARY ~ IF dest=NIL THEN rosaryOfNil ELSE dest;
d0: INT ~ 0;
d1: INT ~ destStart;
d2: INT ~ d1+destLen;
d3: INT ~ destSize;
s: ROSARY ~ IF source=NIL THEN rosaryOfNil ELSE source;
s0: INT ~ sourceStart;
s1: INT ~ s0+sourceLen;
rosary ← Rosary.CatSegments[[d, d0, d1-d0], [s, s0, s1-s0], [d, d2, d3-d2]];
IF NOT Rosary.MapRuns[[rosary], notNil] THEN rosary ← NIL;
};
RosaryMapRuns: PROC [rosary: ROSARY, start, len: INT, action: Rosary.RunActionType] ~ {
IF rosary=NIL
THEN [] ← action[NIL, len]
ELSE [] ← Rosary.MapRuns[[rosary, start, len], action];
};
DoReplace: PROC [root: Node, dest: Text, sourceRope: ROPE, sourceRuns: Runs, sourceCharSets: ROSARY, sourceCharProps: ROSARY, sourceStart, sourceLen: INT, event: Event] RETURNS [result: Text] = {
destRope: ROPE ~ dest.node.rope;
destRuns: Runs ~ dest.node.runs;
destCharSets: ROSARY ~ GetCharSets[dest.node];
destCharProps: ROSARY ~ GetCharProps[dest.node];
destSize: INT ~ Size[dest.node]; -- node size before replacement
size: INT ~ destSize-dest.len+sourceLen; -- node size after replacement
replaceRope, newRope: ROPENIL;
replaceRuns, newRuns: Runs ← NIL;
special: BOOLFALSE;
alreadysaved: BOOL ~ AlreadySaved[dest.node, event];
notify: REF Change.ChangingText ~ IF alreadysaved
THEN Alloc[]
ELSE NEW[Change.ChangingText];
notify^ ← [ChangingText[
root: root,
text: dest.node,
start: dest.start,
newlen: sourceLen,
oldlen: dest.len,
oldRope: destRope,
oldRuns: destRuns,
oldCharSets: destCharSets,
oldCharProps: destCharProps
]];
EditNotify.Notify[notify, before];
IF sourceLen > 0 THEN {
replaceRope ← Rope.Substr[sourceRope, sourceStart, sourceLen];
replaceRuns ← TextLooks.Substr[sourceRuns, sourceStart, sourceLen];
};
SELECT TRUE FROM
(dest.len=0) => { -- doing an insert
SELECT TRUE FROM
(dest.start=0) => { -- insert at start
newRope ← Rope.Concat[replaceRope, destRope];
newRuns ← TextLooks.Concat[replaceRuns, destRuns, sourceLen, destSize];
special ← TRUE;
}
(dest.start=destSize) => { -- insert at end
newRope ← Rope.Concat[destRope, replaceRope];
newRuns ← TextLooks.Concat[destRuns, replaceRuns, destSize, sourceLen];
special ← TRUE;
};
ENDCASE;
(sourceLen=0) => { -- doing a delete
SELECT TRUE FROM
(dest.start=0) => { -- delete from start
newRope ← Rope.Substr[destRope, dest.len, size];
newRuns ← TextLooks.Substr[destRuns, dest.len, size];
special ← TRUE;
}
(dest.end=destSize) => { -- delete from end
newRope ← Rope.Substr[destRope, 0, size];
newRuns ← TextLooks.Substr[destRuns, 0, size];
special ← TRUE;
};
ENDCASE;
};
ENDCASE;
IF NOT special THEN {
newRope ← Rope.Replace[base: destRope, start: dest.start, len: dest.len, with: replaceRope];
newRuns ← TextLooks.Replace[base: destRuns, start: dest.start, len: dest.len, replace: replaceRuns, baseSize: destSize, repSize: sourceLen]
};
dest.node.rope ← newRope;
dest.node.runs ← newRuns;
IF sourceCharSets#NIL OR destCharSets#NIL THEN {
newCharSets: ROSARY ~ RosaryReplace[dest: destCharSets, destSize: destSize, destStart: dest.start, destLen: dest.len, source: sourceCharSets, sourceStart: sourceStart, sourceLen: sourceLen];
NodeProps.PutProp[dest.node, $CharSets, newCharSets];
};
IF sourceCharProps#NIL OR destCharProps#NIL THEN {
newCharProps: ROSARY ~ RosaryReplace[dest: destCharProps, destSize: destSize, destStart: dest.start, destLen: dest.len, source: sourceCharProps, sourceStart: sourceStart, sourceLen: sourceLen];
NodeProps.PutProp[dest.node, $CharProps, newCharProps];
};
NodeAddrs.Replace[dest.node, dest.start, dest.len, sourceLen];
BumpCount[dest.node];
EditNotify.Notify[notify, after];
IF alreadysaved THEN
Free[notify]
ELSE UndoEvent.Note[event, UndoChangeText, notify];
RETURN [[dest.node, dest.start, sourceLen]];
};
UndoChangeText: PROC [undoRef: REF Change, currentEvent: Event] = {
WITH undoRef SELECT FROM
x: REF Change.ChangingText => {
must replace all of text since only save first change per event
[] ← DoReplace[
root: x.root,
dest: [x.text, 0, Size[x.text]],
sourceRope: x.oldRope,
sourceRuns: x.oldRuns,
sourceCharSets: x.oldCharSets,
sourceCharProps: x.oldCharProps,
sourceStart: 0,
sourceLen: Rope.Size[x.oldRope],
event: currentEvent
];
};
ENDCASE => ERROR;
};
DoDelete: PROC [root: Node, text: Text, event: Event] RETURNS [result: Text] ~ {
IF text.len=0 THEN RETURN[text]
ELSE RETURN DoReplace[
root: root,
dest: text,
sourceRope: NIL,
sourceRuns: NIL,
sourceCharSets: NIL,
sourceCharProps: NIL,
sourceStart: 0,
sourceLen: 0,
event: event
];
};
DeleteText: PUBLIC PROC [root: Node, text: Text, event: Event] = {
text ← ClipText[text, Size[text.node]];
IF text.len=0 THEN NULL ELSE [] ← DoDelete[root, text, event];
};
ReplaceText: PUBLIC PROC [destRoot, sourceRoot: Node, dest: Text, source: Text, event: Event] RETURNS [result: Text] = {
-- replace the dest text by a copy of the source text
-- addrs that are in the replaced text move to destStart
-- addrs that are after the replaced text are adjusted
dest ← ClipText[dest, Size[dest.node]];
source ← ClipText[source, Size[source.node]];
IF source.len=0
THEN RETURN DoDelete[destRoot, dest, event]
ELSE RETURN DoReplace[
root: destRoot,
dest: dest,
sourceRope: source.node.rope,
sourceRuns: source.node.runs,
sourceCharSets: GetCharSets[source.node],
sourceCharProps: GetCharProps[source.node],
sourceStart: source.start,
sourceLen: source.len,
event: event
];
};
CopyText: PUBLIC PROC [destRoot, sourceRoot: Node, dest: Location, source: Text, event: Event] RETURNS [result: Text] = {
RETURN ReplaceText[destRoot: destRoot, sourceRoot: sourceRoot, dest: [dest.node, dest.where, 0], source: source, event: event];
};
MoveText: PUBLIC PROC [destRoot, sourceRoot: Node, dest: Location, source: Text, event: Event] RETURNS [result: Text] = {
dest ← ClipLocation[dest, Size[dest.node]];
source ← ClipText[source, Size[source.node]];
IF source.len=0 THEN RETURN [[dest.node, dest.where, 0]];
Move empty source to destination.
IF source.node=dest.node
THEN { -- move text within a single node
MoveAndPinAddrs: PROC [addr: REF, location: INT] RETURNS [quit: BOOLFALSE] = {
Move addr to new location and pin so it won't be changed by Replace.
IF location IN [source.start..source.end) THEN {
new: INT ← location-source.start+dest.where;
IF dest.where>source.start THEN new ← new-source.len; -- move toward end of node
NodeAddrs.PutTextAddr[n: dest.node, addr: addr, location: new, pin: TRUE];
};
};
IF dest.where IN [source.start..source.end] THEN RETURN [source];
Source is already in the right place.
[] ← NodeAddrs.MapTextAddrs[dest.node, MoveAndPinAddrs];
result ← CopyText[destRoot, sourceRoot, dest, source, event];
IF dest.where<source.start
THEN source.start ← source.start+source.len
Move toward start of node: Copy has shifted the source to the right.
ELSE result.start ← dest.where-source.len;
Move toward end of node: Delete will shift the result to the left.
DeleteText[sourceRoot, source, event];
NodeAddrs.UnpinAll[dest.node];
}
ELSE { -- move text from one node to another
MoveAddrs: PROC [addr: REF, location: INT] RETURNS [quit: BOOLFALSE] = {
IF location IN [source.start..source.end) THEN {
new: INT ~ location-source.start+dest.where;
NodeAddrs.MoveTextAddr[from: source.node, to: dest.node, addr: addr, location: new];
};
};
[] ← NodeAddrs.MapTextAddrs[source.node, MoveAddrs];
result ← CopyText[destRoot, sourceRoot, dest, source, event];
DeleteText[sourceRoot, source, event];
};
};
TransposeText: PUBLIC PROC [alphaRoot, betaRoot: Node, alpha: Text, beta: Text, event: Event] RETURNS [alphaResult, betaResult: Text] = {
switched: BOOLFALSE;
alpha ← ClipText[alpha, Size[alpha.node]];
beta ← ClipText[beta, Size[beta.node]];
IF alpha.node=beta.node
THEN { -- transpose within the same node
rootsMatch: Assertion ~ (alphaRoot=betaRoot);
root: Node ~ alphaRoot; node: Node ~ alpha.node;
IF alpha.start<beta.start OR (alpha.start=beta.start AND alpha.end<=beta.end)
THEN NULL -- alpha is to the "left" of beta
ELSE { temp: Text ~ alpha; alpha ← beta; beta ← temp; switched ← TRUE };
SELECT TRUE FROM
alpha.end=beta.start => { -- adjacent
betaResult ← MoveText[root, root, [node, alpha.start], beta, event];
alphaResult ← [node, alpha.start+beta.len, alpha.len];
};
beta.start<alpha.end => { -- overlapping
IF beta.end<alpha.end
THEN { alphaResult ← alpha; betaResult ← beta } -- alpha contains beta
ELSE { -- order is alpha.start, beta.start, alpha.end, beta.end
alphaHead: Text ~ [node, alpha.start, beta.start-alpha.start];
betaTail: Text ~ [node, alpha.end, beta.end-alpha.end];
[] ← TransposeText[root, root, alphaHead, betaTail, event];
alphaResult ← [node, alpha.start+betaTail.len, alpha.len];
betaResult ← [node, alpha.start, beta.len];
};
};
ENDCASE => { -- disjoint
alphaResult ← MoveText[root, root, [node, beta.end], alpha, event];
beta.start ← beta.start-alpha.len; -- moving alpha behind beta shifts beta left
betaResult ← MoveText[root, root, [node, alpha.start], beta, event];
};
}
ELSE { -- transpose between two different nodes
alphaResult ← MoveText[betaRoot, alphaRoot, [beta.node, beta.start], alpha, event];
beta.start ← beta.start+alpha.len; -- moving alpha in front of beta shifts beta right
betaResult ← MoveText[alphaRoot, betaRoot, [alpha.node, alpha.start], beta, event];
};
IF switched THEN RETURN[betaResult, alphaResult];
};
MoveTextOnto: PUBLIC PROC [destRoot, sourceRoot: Node, dest, source: Text, event: Event] RETURNS [result: Text] = {
dest ← ClipText[dest, Size[dest.node]]; source ← ClipText[source, Size[source.node]];
IF dest.node=source.node THEN { -- same node, check for overlap
rootsMatch: Assertion ~ (destRoot=sourceRoot);
root: Node ~ destRoot; node: Node ~ dest.node;
IF dest.end>source.end THEN { -- delete part of dest after source
afterStart: INT ~ MAX[dest.start, source.end];
afterLen: INT ~ dest.end-afterStart;
DeleteText[root, [node, afterStart, afterLen], event];
};
IF dest.start<source.start THEN { -- delete part of dest before source
beforeStart: INT ~ dest.start;
beforeLen: INT ~ MIN[dest.end, source.start]-beforeStart;
DeleteText[root, [node, beforeStart, beforeLen], event];
source.start ← source.start-beforeLen; -- deleting before source shifts source left
};
IF dest.start IN[source.start..source.end] THEN result ← source
ELSE result ← MoveText[root, root, [node, dest.start], source, event];
}
ELSE { -- different nodes
DeleteText[destRoot, dest, event];
result ← MoveText[destRoot, sourceRoot, [dest.node, dest.start], source, event];
};
};
CharSetsForReplace: PROC [charSet: CharSet, replaceSize: INT, destCharSets: ROSARY, destSize, destStart, destLen: INT] RETURNS [ROSARY] ~ {
IF charSet=0 THEN RETURN [NIL]
ELSE {
item: REF CharSet ← NIL;
IF destCharSets#NIL THEN {
Try to find a matching item in destCharSets adjacent to the replacement.
Try: TYPE ~ {before, after};
tries: ARRAY Try OF INT ← [before: destStart-1, after: destStart+destLen];
FOR try: Try IN Try WHILE item=NIL DO
i: INT ~ tries[try];
IF i IN [0..destSize) THEN WITH Rosary.Fetch[destCharSets, i] SELECT FROM
destItem: REF CharSet => IF destItem^=charSet THEN item ← destItem;
ENDCASE;
ENDLOOP;
};
IF item=NIL THEN item ← NEW[CharSet ← charSet];
RETURN [Rosary.FromItem[item, replaceSize]];
};
};
ReplaceByRope: PUBLIC PROC [root: Node, dest: Text, rope: ROPE, inherit: BOOL, looks: Looks, charSet: CharSet, event: Event] RETURNS [result: Text] = {
destSize: INT ~ Size[dest.node];
ropeSize: INT ~ Rope.Size[rope];
dest ← ClipText[dest, destSize];
IF dest.len#0 OR ropeSize#0 THEN {
destRope: ROPE ~ dest.node.rope;
destRuns: Runs ~ dest.node.runs;
destCharSets: ROSARY ~ GetCharSets[dest.node];
destCharProps: ROSARY ~ GetCharProps[dest.node];
alreadysaved: BOOL ~ AlreadySaved[dest.node, event];
notify: REF Change.ChangingText ~ (
IF alreadysaved THEN Alloc[]
ELSE NEW[Change.ChangingText]
);
notify^ ← [ChangingText[
root: root,
text: dest.node,
start: dest.start,
newlen: ropeSize,
oldlen: dest.len,
oldRope: destRope,
oldRuns: destRuns,
oldCharSets: destCharSets,
oldCharProps: destCharProps
]];
EditNotify.Notify[notify, before];
dest.node.rope ← Rope.Replace[base: destRope, start: dest.start, len: dest.len, with: rope];
dest.node.runs ← TextLooks.ReplaceByRun[destRuns, dest.start, dest.len, ropeSize, destSize, inherit, looks];
NodeAddrs.Replace[dest.node, dest.start, dest.len, ropeSize];
BumpCount[dest.node];
IF dest.node.hascharsets OR charSet#0 THEN {
sourceCharSets: ROSARY ~ IF charSet=0 THEN NIL ELSE CharSetsForReplace[charSet: charSet, replaceSize: ropeSize, destCharSets: destCharSets, destSize: destSize, destStart: dest.start, destLen: dest.len];
newCharSets: ROSARY ~ RosaryReplace[dest: destCharSets, destSize: destSize, destStart: dest.start, destLen: dest.len, source: sourceCharSets, sourceStart: 0, sourceLen: ropeSize];
NodeProps.PutProp[dest.node, $CharSets, newCharSets];
};
IF dest.node.hascharprops THEN {
newCharProps: ROSARY ~ RosaryReplace[dest: destCharProps, destSize: destSize, destStart: dest.start, destLen: dest.len, source: NIL, sourceStart: 0, sourceLen: ropeSize];
NodeProps.PutProp[dest.node, $CharProps, newCharProps];
};
EditNotify.Notify[notify, after];
IF alreadysaved THEN Free[notify]
ELSE UndoEvent.Note[event, UndoChangeText, notify];
};
RETURN [[dest.node, dest.start, ropeSize]];
};
InsertRope: PUBLIC PROC [root: Node, dest: Location, rope: ROPE,
inherit: BOOLTRUE, looks: Looks ← noLooks, charSet: CharSet ← 0,
event: Event ← NIL] RETURNS [result: Text]
= { RETURN ReplaceByRope[root: root, dest: [dest.node, dest.where, 0], rope: rope, inherit: inherit, looks: looks, charSet: charSet, event: event] };
AppendRope: PUBLIC PROC [root: Node, dest: Node, rope: ROPE,
inherit: BOOLTRUE, looks: Looks ← noLooks, charSet: CharSet ← 0,
event: Event ← NIL] RETURNS [result: Text]
= { RETURN ReplaceByRope[root: root, dest: [dest, maxLen], rope: rope, inherit: inherit, looks: looks, charSet: charSet, event: event] };
ReplaceByChar: PUBLIC PROC [root: Node, dest: Text, char: CHAR, inherit: BOOLTRUE, looks: Looks ← noLooks, charSet: CharSet ← 0, event: Event ← NIL] RETURNS [result: Text] = {
RETURN ReplaceByRope[root: root, dest: dest, rope: Rope.FromChar[char], inherit: inherit, looks: looks, charSet: charSet, event: event];
};
InsertChar: PUBLIC PROC [root: Node, dest: Location, char: CHAR,
inherit: BOOLTRUE, looks: Looks ← noLooks, charSet: CharSet ← 0,
event: Event ← NIL] RETURNS [result: Text]
= { RETURN ReplaceByChar[root: root, dest: [dest.node, dest.where, 0], char: char, inherit: inherit, looks: looks, charSet: charSet, event: event] };
AppendChar: PUBLIC PROC [root: Node, dest: Node, char: CHAR,
inherit: BOOLTRUE, looks: Looks ← noLooks, charSet: CharSet ← 0,
event: Event ← NIL] RETURNS [result: Text]
= { RETURN ReplaceByChar[root: root, dest: [dest, maxLen, 0], char: char, inherit: inherit, looks: looks, charSet: charSet, event: event] };
RopeFromString: PROC [string: String] RETURNS [rope: ROPENIL] ~ {
string ← ClipString[string];
IF string.len>0 THEN {
i: NAT ← string.start;
p: PROC RETURNS [CHAR] ~ { c: CHAR ~ string.text[i]; i ← i+1; RETURN[c] };
rope ← Rope.FromProc[string.len, p];
};
};
ReplaceByString: PUBLIC PROC [root: Node, dest: Text, string: String,
inherit: BOOLTRUE, looks: Looks ← noLooks, charSet: CharSet ← 0,
event: Event ← NIL] RETURNS [result: Text] = {
RETURN ReplaceByRope[root: root, dest: dest, rope: RopeFromString[string], inherit: inherit, looks: looks, charSet: charSet, event: event];
};
InsertString: PUBLIC PROC [root: Node, dest: Location, string: String,
inherit: BOOLTRUE, looks: Looks ← noLooks, charSet: CharSet ← 0,
event: Event ← NIL] RETURNS [result: Text]
= { RETURN ReplaceByString[root: root, dest: [dest.node, dest.where, 0], string: string, inherit: inherit, looks: looks, charSet: charSet, event: event] };
AppendString: PUBLIC PROC [root: Node, dest: Node, string: String,
inherit: BOOLTRUE, looks: Looks ← noLooks, charSet: CharSet ← 0,
event: Event ← NIL] RETURNS [result: Text]
= { RETURN ReplaceByString[root: root, dest: [dest, maxLen, 0], string: string, inherit: inherit, looks: looks, charSet: charSet, event: event] };
Operation to add or delete looks
ChangeLooks: PUBLIC PROC [root: Node, text: Text, remove, add: Looks, event: Event] = {
-- first remove then add in the given range
size: INT ~ Size[text.node];
text ← ClipText[text, size];
IF text.len>0 THEN {
oldRuns: Runs ~ text.node.runs;
newRuns: Runs ~ TextLooks.ChangeLooks[oldRuns, size, remove, add, text.start, text.len];
IF newRuns=oldRuns THEN NULL -- no change
ELSE {
alreadysaved: BOOL ~ AlreadySaved[text.node, event];
notify: REF Change.ChangingText ~ IF alreadysaved THEN Alloc[] ELSE NEW[Change.ChangingText];
notify^ ← [ChangingText[root, text.node, text.start, text.len, text.len, text.node.rope, oldRuns, GetCharSets[text.node], GetCharProps[text.node]]];
EditNotify.Notify[notify, before];
text.node.runs ← newRuns;
BumpCount[text.node];
EditNotify.Notify[notify, after];
IF alreadysaved THEN Free[notify]
ELSE UndoEvent.Note[event, UndoChangeText, notify];
};
};
};
AddLooks: PUBLIC PROC [root: Node, text: Text, add: Looks, event: Event]
= { ChangeLooks[root, text, noLooks, add, event] };
RemoveLooks: PUBLIC PROC [root: Node, text: Text, remove: Looks, event: Event]
= { ChangeLooks[root, text, remove, noLooks, event] };
SetLooks: PUBLIC PROC [root: Node, text: Text, new: Looks, event: Event]
= { ChangeLooks[root, text, allLooks, new, event] };
ClearLooks: PUBLIC PROC [root: Node, text: Text, event: Event]
= { ChangeLooks[root, text, allLooks, noLooks, event] };
Character Properties
Note: Character properties are implemented using a ROSARY (an immutable sequence of REFs; see Rosary.mesa), appearing in the CharProps property of the node. The values in this rosary are of type Atom.PropList, but don't use Atom.PutPropOnList, since we regard these lists as immutable. The values on the property lists are externalized and internalized using the procedures registered with NodeProps, but no warranty is expressed or implied for props that are not immutable.
This property list mechanism regards a PropList as an immutable value. We don't use the procedures from Atom since 1) they might smash an existing PropList, and 2) they hold a global monitor in AtomImpl.
PropList: TYPE ~ Atom.PropList;
GetCharProp: PUBLIC PROC [node: Node, index: INT, name: ATOM] RETURNS [value: REF] ~ {
RETURN [GetPropFromList[GetCharPropList[node, index], name]];
};
PutCharProp: PUBLIC PROC [node: Node, index: INT, name: ATOM, value: REF, nChars: INT, event: Event, root: Node] ~ {
size: INT ~ Size[node];
oldCharProps: ROSARY ~ GetCharProps[node];
p: PROC[q: PROC[REF, INT]] ~ {
action: Rosary.RunActionType ~ {
oldList: PropList ~ NARROW[item];
q[PutPropOnList[oldList, name, value], repeat];
};
RosaryMapRuns[oldCharProps, index, nChars, action];
};
replaceCharProps: ROSARY ~ Rosary.FromProcProc[p];
newCharProps: ROSARY ~ RosaryReplace[dest: oldCharProps, destSize: size, destStart: index, destLen: nChars, source: replaceCharProps, sourceStart: 0, sourceLen: nChars];
PutProp[node, $CharProps, newCharProps, event, root];
};
GetCharPropList: PUBLIC PROC [node: Node, index: INT] RETURNS [PropList] ~ {
charProps: ROSARY ~ GetCharProps[node];
IF charProps=NIL THEN RETURN[NIL]
ELSE RETURN[NARROW[Rosary.Fetch[charProps, index]]];
};
PutCharPropList: PUBLIC PROC [node: Node, index: INT, propList: PropList, nChars: INT, event: Event, root: Node] ~ {
size: INT ~ Size[node];
oldCharProps: ROSARY ~ GetCharProps[node];
replaceCharProps: ROSARY ~ Rosary.FromItem[propList, nChars];
newCharProps: ROSARY ~ RosaryReplace[dest: oldCharProps, destSize: size, destStart: index, destLen: nChars, source: replaceCharProps, sourceStart: 0, sourceLen: nChars];
PutProp[node, $CharProps, newCharProps, event, root];
};
GetPropFromList: PUBLIC PROC [propList: PropList, key: ATOM] RETURNS [value: REF] ~ {
FOR list: PropList ← propList, list.rest UNTIL list=NIL DO
IF list.first.key=key THEN RETURN[list.first.val];
ENDLOOP;
RETURN[NIL];
};
PutPropOnList: PUBLIC PROC [propList: PropList, key: ATOM, value: REF] RETURNS [new: PropList] ~ {
RemoveKeyFrom: PROC [p: PropList] RETURNS [PropList] = {
IF p = NIL THEN RETURN [NIL]
ELSE IF p.first.key = key THEN RETURN [p.rest]
ELSE {
rest: PropList ← RemoveKeyFrom[p.rest];
IF rest # p.rest THEN RETURN [CONS[p.first, rest]]
ELSE RETURN [p]
}
};
new ← RemoveKeyFrom[propList];
IF value # NIL THEN new ← CONS[NEW[Atom.DottedPairNode ← [key, value]], new];
};
MapCharProps: PUBLIC PROC [node: Node, index: INT, action: MapPropsAction] RETURNS [quit: BOOL] ~ {
FOR list: PropList ← GetCharPropList[node, index], list.rest UNTIL list=NIL DO
IF action[NARROW[list.first.key], list.first.val].quit THEN RETURN[TRUE];
ENDLOOP;
RETURN[FALSE];
};
ModifyCharProps: PUBLIC PROC [node: Node, name: ATOM, index: INT, nChars: INT, action: ModifyPropsAction, event: Event, root: Node] RETURNS [quit: BOOL] ~ {
prefixNIL: INT ~ INT.LAST;
prefixSize: INT ← prefixNIL;
oldCharProps: ROSARY ~ GetCharProps[node];
i: INT ← index;
p: PROC[q: PROC[REF, INT]] ~ {
runAction: Rosary.RunActionType ~ {
oldList: PropList ~ NARROW[item];
oldValue: REF ~ GetPropFromList[oldList, name];
stop: BOOLFALSE; newValue: REFNIL;
[stop, newValue] ← action[oldValue, i, repeat];
IF newValue # oldValue AND prefixSize = prefixNIL THEN prefixSize ← i;
IF prefixSize # prefixNIL THEN {
newList: PropList ~ IF newValue = oldValue THEN oldList ELSE PutPropOnList[oldList, name, newValue];
q[newList, repeat];
};
i ← i + repeat;
IF stop THEN RETURN [TRUE];
};
RosaryMapRuns[oldCharProps, index, nChars, runAction];
};
replaceCharProps: ROSARY ~ Rosary.FromProcProc[p]; -- sets prefixSize and i as side effect
IF prefixSize # prefixNIL THEN {
replaceSize: INT ~ i-prefixSize;
newCharProps: ROSARY ~ RosaryReplace[dest: oldCharProps, destSize: Size[node], destStart: prefixSize, destLen: replaceSize, source: replaceCharProps, sourceStart: 0, sourceLen: replaceSize];
IF Rosary.Size[replaceCharProps] # replaceSize THEN ERROR;
PutProp[node, $CharProps, newCharProps, event, root];
};
};
Changing Style / Format of node
ChangeFormat: PUBLIC PROC [node: Node, formatName: ATOM, event: Event ← NIL, root: Node ← NIL] = {
notify: REF Change.ChangingFormat;
oldFormatName: ATOM ← node.formatName;
IF node=NIL THEN RETURN;
IF root=NIL THEN root ← TextNode.Root[node];
IF oldFormatName = formatName THEN RETURN; -- no change
notify ← NEW[Change.ChangingFormat ←
[ChangingFormat[root, node, formatName, oldFormatName]]];
EditNotify.Notify[notify, before];
node.formatName ← formatName;
EditNotify.Notify[notify, after];
IF event#NIL THEN UndoEvent.Note[event, UndoChangeFormat, notify];
};
UndoChangeFormat: PROC [undoRef: REF, currentEvent: Event] = {
WITH undoRef SELECT FROM
x: REF Change.ChangingFormat => {
ChangeFormat[x.node, x.oldFormatName, currentEvent, x.root];
};
ENDCASE => ERROR;
};
quote: ROPE ~ "\"";
ChangeStyle: PUBLIC PROC [node: Node, name: ROPE, event: Event ← NIL, root: Node ← NIL] = {
newprefix: ROPENIL;
IF node=NIL THEN RETURN;
IF Rope.Size[name] > 0 THEN newprefix ← Rope.Cat[quote, name, quote, " style"];
PutProp[node, $Prefix, newprefix, event, root];
};
PutProp: PUBLIC PROC [node: Node, name: ATOM, value: REF, event: Event ← NIL, root: Node ← NIL] = {
IF node=NIL THEN NULL
ELSE {
notify: REF Change.ChangingProp;
oldval: REF ~ NodeProps.GetProp[node, name];
IF oldval=value THEN RETURN;
IF root=NIL THEN root ← TextNode.Root[node];
notify ← NEW[Change.ChangingProp ← [ChangingProp[root, node, Atom.GetPName[name], name, value, oldval]]];
EditNotify.Notify[notify, before];
NodeProps.PutProp[node, name, value];
EditNotify.Notify[notify, after];
IF event#NIL THEN UndoEvent.Note[event, UndoPutProp, notify];
};
};
UndoPutProp: PROC [undoRef: REF Change, currentEvent: Event] = {
WITH undoRef SELECT FROM
x: REF Change.ChangingProp => PutProp[node: x.node, name: x.propAtom, value: x.oldval, event: currentEvent, root: x.root];
ENDCASE => ERROR;
};
GetProp: PUBLIC PROC [node: Node, name: ATOM] RETURNS [value: REF] = {
RETURN [NodeProps.GetProp[node, name]];
};
Operations to create a text node
FromRope: PUBLIC PROC [rope: ROPE] RETURNS [new: Node] = {
-- create a text node with looks from a normal rope
new ← TextNode.NewTextNode[];
new.rope ← rope; new.last ← TRUE;
new.runs ← TextLooks.CreateRun[Rope.Size[new.rope]];
};
FromString: PUBLIC PROC [string: REF READONLY TEXT]
RETURNS [new: Node] = {
-- copies the contents of the string
new ← TextNode.NewTextNode[]; new.last ← TRUE;
new.rope ← Rope.FromRefText[string];
new.runs ← TextLooks.CreateRun[Rope.Size[new.rope]];
};
DocFromNode: PUBLIC PROC [child: Node] RETURNS [new: Node] = {
new ← TextNode.NewTextNode[];
new.child ← child; new.last ← TRUE;
child.next ← new; child.last ← TRUE;
};
Caps and Lowercase
ChangeCaps: PUBLIC PROC [root: Node, dest: Text, how: CapChange, event: Event] = {
notify: REF Change.ChangingText;
oldRope, destRope: ROPE;
rdr: RopeReader.Ref;
initCap: BOOLTRUE;
destSize, start, len: INT;
alreadysaved: BOOL;
destSize ← Rope.Size[oldRope ← destRope ← dest.node.rope];
dest ← ClipText[dest, destSize];
start ← dest.start;
IF (len ← dest.len)=0 THEN RETURN;
notify ← IF (alreadysaved ← AlreadySaved[dest.node, event]) THEN Alloc[]
ELSE NEW[Change.ChangingText];
notify^ ← [ChangingText[root, dest.node, start, len, len, destRope, dest.node.runs]];
EditNotify.Notify[notify, before];
rdr ← RopeReader.GetRopeReader[];
UNTIL len=0 DO
c: CHAR;
maxText: NAT = 1000;
num: NATMIN[len, maxText];
new: REF TEXTNEW[TEXT[num]];
RopeReader.SetPosition[rdr, destRope, start];
SELECT how FROM
firstCap => IF num > 0 THEN {
new[0] ← RopeEdit.UpperCase[RopeReader.Get[rdr]];
FOR i:NAT IN [1..num) DO
new[i] ← RopeEdit.LowerCase[RopeReader.Get[rdr]];
ENDLOOP };
allCaps => FOR i:NAT IN [0..num) DO
new[i] ← RopeEdit.UpperCase[RopeReader.Get[rdr]];
ENDLOOP;
allLower => FOR i:NAT IN [0..num) DO
new[i] ← RopeEdit.LowerCase[RopeReader.Get[rdr]];
ENDLOOP;
initCaps => {
init: BOOLTRUE;
FOR i:NAT IN [0..num) DO
SELECT c ← RopeReader.Get[rdr] FROM
IN ['a..'z] => {
new[i] ← IF init THEN c-40B ELSE c; -- force first upper
init ← FALSE };
IN ['A..'Z] => {
new[i] ← IF init THEN c ELSE c+40B; -- force others lower
init ← FALSE };
ENDCASE => {
new[i] ← c;
init ← c # '\' AND ~RopeEdit.AlphaNumericChar[c] };
ENDLOOP };
ENDCASE => ERROR;
new.length ← num;
destRope ← RopeEdit.ReplaceByTEXT[destRope, new, 0, num, start, num, destSize];
IF Rope.Size[destRope] # destSize THEN ERROR;
len ← len-num; start ← start+num;
ENDLOOP;
dest.node.rope ← destRope;
RopeReader.FreeRopeReader[rdr];
EditNotify.Notify[notify, after];
IF alreadysaved THEN Free[notify]
ELSE UndoEvent.Note[event, UndoChangeText, notify];
};
AllCaps: PUBLIC PROC [root: Node, dest: Text, event: Event]
= { ChangeCaps[root, dest, allCaps, event] };
AllLower: PUBLIC PROC [root: Node, dest: Text, event: Event]
= { ChangeCaps[root, dest, allLower, event] };
InitialCaps: PUBLIC PROC [root: Node, dest: Text, event: Event]
= { ChangeCaps[root, dest, initCaps, event] };
Miscellaneous
BumpCount: PROC [text: Node] = {
countLimit: INT ← 20;
count: [0..TextNode.countMax];
IF text=NIL THEN RETURN;
IF (count←text.count) = TextNode.countMax OR (text.count ← count+1) >= countLimit THEN
-- see if need to flatten
IF Flatten[text] THEN text.count ← 0
ELSE text.count ← text.count/2;
};
debug: BOOLFALSE;
Flatten: PUBLIC PROC [text: Node] RETURNS [BOOL] = {
looksSize, looksPieces, looksDepth: INT ← 0;
ropeFlag, looksFlag: BOOLFALSE;
rope: ROPE ← text.rope;
runs: Runs ← text.runs;
ropeSize: INT ~ Rope.Size[rope];
[looksSize, looksPieces, looksDepth] ← TextLooks.LooksStats[runs];
-- size = # of characters
-- pieces = # terminal nodes in tree
-- depth = max depth of tree
IF ropeSize=0 AND looksSize=0 THEN RETURN [FALSE];
looksFlag ← (looksPieces > 20 AND looksSize < 5*looksPieces) OR looksDepth > 100 OR
(SELECT looksPieces/8 FROM
< 2 => looksDepth/4 > looksPieces,
< 6 => looksDepth/2 > looksPieces,
< 12 => looksDepth > looksPieces,
ENDCASE => looksDepth > looksPieces/2);
IF ropeFlag OR looksFlag THEN {
IF debug THEN { CheckNode.CheckRope[rope]; CheckNode.CheckRuns[runs] };
text.rope ← IF ropeSize>256 THEN Rope.Balance[rope] ELSE Rope.Flatten[rope];
text.runs ← TextLooks.Flatten[runs];
IF debug THEN { CheckNode.CheckRope[text.rope]; CheckNode.CheckRuns[text.runs] };
RETURN [TRUE];
};
RETURN [FALSE];
};
maxSearchDepth: NAT ← 20;
AlreadySaved: PUBLIC PROC [text: Node, event: Event] RETURNS [BOOL] = {
returns TRUE if there is already a ChangingText record for given node in the event
k: NAT ← maxSearchDepth;
IF event = NIL THEN RETURN [TRUE];
FOR l: UndoEvent.SubEvent ← event.subevents, l.next UNTIL l=NIL DO
undoRef: REF Change ~ l.undoRef;
WITH undoRef SELECT FROM
x: REF Change.ChangingText => IF x.text = text THEN RETURN[TRUE];
ENDCASE;
IF (k ← k-1) = 0 THEN EXIT;
Don't keep searching too long; give up and make a new event instead. - mfp
ENDLOOP;
RETURN [FALSE];
};
END.