-- TextEditImpl.mesa
-- 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
DIRECTORY
TextEdit,
EditNotify,
UndoEvent,
NodeProps,
RopeEdit,
RopeFrom,
RopeReader,
TextLooks,
TextNode,
NodeAddrs,
Atom,
Rope, 
CheckNode;
TextEditImpl: 
CEDAR MONITOR
IMPORTS npI:NodeProps, RopeEdit, TextLooks, EditNotify, UndoEvent, NodeAddrs,
TextNode, RopeFrom, RopeReader, Rope, Atom, CheckNode
 
EXPORTS TextEdit =
 
BEGIN
OPEN TextEdit, EditNotify;
Runs: TYPE = TextLooks.Runs;
-- **** Cache of notify records ****
scratch1, scratch2, scratch3: REF ChangingText Change;
Create: 
PROC 
RETURNS [
REF ChangingText Change] = {
RETURN [TextNode.pZone.NEW[ChangingText Change]] };
 
Alloc: 
ENTRY PROC 
RETURNS [scratch: 
REF ChangingText Change] = {
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 ← Create[] };
 
Free: 
ENTRY PROC [scratch: 
REF ChangingText Change] = {
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 };
 
-- **** Text Editing Operations ****
DoReplace: 
PROC [root: Ref,
dest: RefTextNode, destStart, destLen, destSize: Offset,
sourceRope: ROPE, sourceRuns: Runs,
sourceStart, sourceLen: Offset, event: Event]
RETURNS [resultStart, resultLen: Offset] = {
destRope, replaceRope, newRope: ROPE;
size: Offset;
notify: REF ChangingText Change;
alreadysaved: BOOLEAN;
destRuns, replaceRuns, newRuns: Runs;
special: BOOLEAN;
size ← destSize-destLen+sourceLen;
destRuns ← dest.runs; destRope ← dest.rope;
notify ← 
IF (alreadysaved ← AlreadySaved[dest,event]) 
THEN Alloc[]
ELSE TextNode.pZone.NEW[ChangingText Change];
 
notify^ ← [ChangingText[root,dest,destStart,sourceLen,destLen,destRope,destRuns]];
Notify[notify,before];
IF sourceLen > 0 
THEN {
replaceRope ← RopeEdit.Substr[sourceRope,sourceStart,sourceLen];
replaceRuns ← TextLooks.Substr[sourceRuns,sourceStart,sourceLen]};
 
special ← FALSE;
IF destLen=0 
THEN { 
-- doing an insert
IF destStart=0 
THEN { 
-- insert at start
newRope ← RopeEdit.Concat[replaceRope,destRope,sourceLen,destSize];
newRuns ← TextLooks.Concat[replaceRuns,destRuns,sourceLen,destSize];
special ← TRUE }
 
ELSE 
IF destStart=destSize 
THEN { 
-- insert at end
newRope ← RopeEdit.Concat[destRope,replaceRope,destSize,sourceLen];
newRuns ← TextLooks.Concat[destRuns,replaceRuns,destSize,sourceLen];
special ← TRUE }}
 
 
ELSE 
IF sourceLen=0 
THEN { 
-- doing a delete
IF destStart=0 
THEN { 
-- delete from start
newRope ← RopeEdit.Substr[destRope,destLen,size];
newRuns ← TextLooks.Substr[destRuns,destLen,size];
special ← TRUE }
 
ELSE 
IF (destStart+destLen)=destSize 
THEN { 
-- delete from end
newRope ← RopeEdit.Substr[destRope,0,size];
newRuns ← TextLooks.Substr[destRuns,0,size];
special ← TRUE }};
 
 
IF ~special 
THEN {
newRope ← RopeEdit.Replace[destRope,destStart,destLen,
replaceRope,destSize,sourceLen];
 
newRuns ← TextLooks.Replace[destRuns,destStart,destLen,
replaceRuns,destSize,sourceLen] };
 
 
dest.rope ← newRope; dest.runs ← newRuns;
NodeAddrs.Replace[dest,destStart,destLen,sourceLen];
BumpCount[dest];
Notify[notify,after];
IF alreadysaved THEN Free[notify]
ELSE UndoEvent.Note[event,UndoChangeText,notify];
RETURN [destStart,sourceLen] };
 
MoveText: 
PUBLIC 
PROC [
destRoot, sourceRoot: Ref,
dest: RefTextNode, destLoc: Offset ← 0,
source: RefTextNode, start: Offset ← 0, len: Offset ← MaxLen,
event: Event ← NIL] 
RETURNS [resultStart, resultLen: Offset] = {
MoveAddrs: 
PROC [addr: 
REF, location: Offset] 
RETURNS [quit: 
BOOLEAN] = {
IF location NOT IN [start..start+len) THEN RETURN [FALSE];
IF dest=source 
THEN { 
-- pin addr at new location
new: Offset ← destLoc+location-start;
IF destLoc > start THEN new ← new-len; -- moving toward end of node
NodeAddrs.PutTextAddr[dest, addr, new];
NodeAddrs.PinTextAddr[dest, addr] -- so it won't be changed by Replace -- }
 
ELSE NodeAddrs.MoveTextAddr[ 
-- move the addr to the new node
from: source, to: dest, addr: addr, location: destLoc+location-start];
 
RETURN [FALSE] };
 
destSize, sourceSize: Offset;
sourceSize ← RopeEdit.Size[source.rope];
start ← MIN[MAX[0,start],sourceSize];
len ← MIN[MAX[0,len],sourceSize-start];
destSize ← IF dest=source THEN sourceSize ELSE RopeEdit.Size[dest.rope];
destLoc ← MIN[MAX[0,destLoc],destSize];
IF source=dest AND destLoc IN [start..start+len] THEN RETURN [start,len];
IF len=0 THEN RETURN [destLoc,0];
[] ← NodeAddrs.MapTextAddrs[source, MoveAddrs];
[resultStart, resultLen] ← CopyText[destRoot, sourceRoot, dest, destLoc, source, start, len, event];
IF dest=source 
THEN { 
-- need to adjust locations
IF destLoc > start THEN resultStart ← destLoc-len -- moving toward end of node
ELSE start ← start+len -- moving toward start of node-- };
 
DeleteText[sourceRoot, source, start, len, event];
IF dest=source THEN NodeAddrs.UnpinAll[source] };
 
TransposeText: 
PUBLIC 
PROC [
alphaRoot, betaRoot: Ref,
alpha: RefTextNode, alphaStart: Offset ← 0, alphaLen: Offset ← MaxLen,
beta: RefTextNode, betaStart: Offset ← 0, betaLen: Offset ← MaxLen, event: Event ← NIL] 
RETURNS [alphaResultStart, alphaResultLen, betaResultStart, betaResultLen: Offset] = {
SwitchResults: 
PROC = {
start, len: Offset;
start ← betaResultStart; len ← betaResultLen;
betaResultStart ← alphaResultStart; betaResultLen ← alphaResultLen;
alphaResultStart ← start; alphaResultLen ← len };
 
alphaSize, betaSize: Offset;
switched: BOOLEAN ← FALSE;
alphaSize ← RopeEdit.Size[alpha.rope];
alphaStart ← MIN[MAX[0,alphaStart],alphaSize];
alphaResultLen ← alphaLen ← MIN[MAX[0,alphaLen], alphaSize-alphaStart];
betaSize ← IF beta=alpha THEN alphaSize ELSE RopeEdit.Size[beta.rope];
betaStart ← MIN[MAX[0,betaStart],betaSize];
betaResultLen ← betaLen ← MIN[MAX[0,betaLen],betaSize-betaStart];
IF alpha=beta 
THEN {
alphaEnd: Offset;
IF alphaStart > betaStart 
THEN { 
-- switch them
start: Offset ← alphaStart;
len: Offset ← alphaLen;
root: Ref ← alphaRoot;
alphaStart ← betaStart; betaStart ← start;
alphaResultLen ← alphaLen ← betaLen;
alphaRoot ← betaRoot; betaRoot ← root;
betaResultLen ← betaLen ← len;
switched ← TRUE };
 
alphaEnd ← alphaStart+alphaLen;
IF alphaEnd = betaStart 
THEN { 
-- turn into a move instead
[] ← MoveText[alphaRoot,betaRoot,alpha,alphaStart,alpha,betaStart,betaLen,event];
betaResultStart ← alphaStart;
alphaResultStart ← alphaStart+betaLen;
IF switched THEN SwitchResults;
RETURN };
 
IF alphaEnd > betaStart 
THEN { 
-- overlapping
overlap, alphaHeadLen, betaTailLen: Offset;
alphaHeadLen ← betaStart-alphaStart;
betaTailLen ← betaStart+betaLen-alphaEnd;
IF alphaHeadLen < 0 
OR betaTailLen < 0 
THEN
RETURN [alphaStart,alphaLen,betaStart,betaLen];
 
overlap ← alphaEnd-betaStart;
[alphaResultStart,alphaResultLen,betaResultStart,betaResultLen] ←
TransposeText[alphaRoot,betaRoot,alpha,alphaStart,alphaHeadLen,
alpha,alphaEnd,betaTailLen,event];
 
 
betaResultLen ← betaResultLen+overlap;
alphaResultStart ← alphaResultStart-overlap;
alphaResultLen ← alphaResultLen+overlap;
IF switched THEN SwitchResults;
RETURN }};
 
 
[alphaResultStart, alphaResultLen] ← MoveText[ 
-- move alpha after beta
destRoot: betaRoot, sourceRoot: alphaRoot,
dest: beta, destLoc: betaStart+betaLen,
source: alpha, start: alphaStart, len: alphaLen, event: event]; 
 
IF alpha=beta THEN betaStart ← betaStart-alphaLen
ELSE alphaResultStart ← alphaResultStart-betaLen;
[betaResultStart, betaResultLen] ← MoveText[ 
-- move beta to old alpha location
destRoot: alphaRoot, sourceRoot: betaRoot,
dest: alpha, destLoc: alphaStart,
source: beta, start: betaStart, len: betaLen, event: event];
 
IF switched THEN SwitchResults };
 
ReplaceByChar: 
PUBLIC 
PROC [
root: Ref, dest: RefTextNode, char: CHAR,
start: Offset ← 0, len: Offset ← MaxLen,
inherit: BOOLEAN ← TRUE, looks: Looks ← noLooks, event: Event ← NIL]
RETURNS [resultStart, resultLen: Offset] = {
-- if inherit is false, char gets specifed looks  
-- if inherit is true, char gets looks in following manner:
-- if dest length is 0, then gets looks from argument list, else
-- if start > 0, then looks and list of previous char,
-- else looks of char following replacement
 
notify: REF ChangingText Change;
destRope, newRope: ROPE;
destSize: Offset;
alreadysaved: BOOLEAN;
IF dest=NIL THEN RETURN [0,0];
destSize ← RopeEdit.Size[destRope ← dest.rope];
start ← MIN[MAX[0,start],destSize];
len ← MIN[MAX[0,len],destSize-start];
notify ← 
IF (alreadysaved ← AlreadySaved[dest,event]) 
THEN Alloc[]
ELSE TextNode.pZone.NEW[ChangingText Change];
 
notify^ ← [ChangingText[root,dest,start,1,len,destRope,dest.runs]];
Notify[notify,before];
newRope ← RopeEdit.ReplaceByChar[destRope,char,start,len,destSize]; 
dest.runs ← TextLooks.ReplaceByRun[dest.runs,start,len,1,destSize,inherit,looks]; 
dest.rope ← newRope;
NodeAddrs.Replace[dest,start,len,1];
BumpCount[dest];
Notify[notify,after];
IF alreadysaved THEN Free[notify]
ELSE UndoEvent.Note[event,UndoChangeText,notify];
RETURN [start,1] };
 
ReplaceByString: 
PUBLIC 
PROC [
root: Ref, dest: RefTextNode, string: REF READONLY TEXT,
stringStart: NAT ← 0, stringNum: NAT ← MaxNat,
start: Offset ← 0, len: Offset ← MaxLen,
inherit: BOOLEAN ← TRUE, looks: Looks ← noLooks, event: Event ← NIL]
RETURNS [resultStart, resultLen: Offset] = {
notify: REF ChangingText Change;
destRope, newRope: ROPE;
alreadysaved: BOOLEAN;
destSize: Offset;
IF dest=NIL THEN RETURN [0,0];
stringNum ← 
IF string=
NIL 
OR stringStart >= string.length 
THEN 0 
ELSE
MIN[stringNum, string.length-stringStart];
 
destSize ← RopeEdit.Size[destRope ← dest.rope];
start ← MIN[MAX[0,start],destSize];
len ← MIN[MAX[0,len],destSize-start];
notify ← 
IF (alreadysaved ← AlreadySaved[dest,event]) 
THEN Alloc[]
ELSE TextNode.pZone.NEW[ChangingText Change];
 
notify^ ← [ChangingText[root,dest,start,stringNum,len,destRope,dest.runs]];
Notify[notify,before];
newRope ← RopeEdit.ReplaceByString[destRope,string,stringStart,stringNum,
start,len,destSize]; 
 
dest.runs ← TextLooks.ReplaceByRun[dest.runs,start,len,stringNum,
destSize,inherit,looks]; 
 
dest.rope ← newRope;
NodeAddrs.Replace[dest,start,len,stringNum];
BumpCount[dest];
Notify[notify,after];
IF alreadysaved THEN Free[notify]
ELSE UndoEvent.Note[event,UndoChangeText,notify];
RETURN [start,stringNum] };
 
UndoChangeText: 
PROC [undoRef: 
REF, currentEvent: Event] = 
TRUSTED {
saved: REF Change ← NARROW[undoRef];
WITH x:saved SELECT FROM
ChangingText => [] ← ReplaceByText[x.root,x.text,0,MaxLen,
x.oldRope,x.oldRuns,0,MaxLen,currentEvent];
-- must replace all of text since only save first change per event
 
ENDCASE => ERROR };
 
ReplaceByText: 
PUBLIC 
PROC [
root: Ref, dest: RefTextNode, destStart: Offset ← 0, destLen: Offset ← MaxLen,
sourceRope: ROPE, sourceRuns: Runs,
sourceStart: Offset ← 0, sourceLen: Offset ← MaxLen, event: Event ← NIL]
RETURNS [resultStart, resultLen: Offset] = {
sourceSize, destSize: Offset;
IF dest=NIL THEN RETURN [0,0];
sourceSize ← RopeEdit.Size[sourceRope];
sourceStart ← MIN[MAX[0,sourceStart],sourceSize];
IF sourceLen # 0 
THEN {
IF sourceStart >= sourceSize THEN sourceStart ← sourceSize;
sourceLen ← MIN[MAX[0,sourceLen], sourceSize-sourceStart]};
 
destSize ← RopeEdit.Size[dest.rope];
destStart ← MIN[MAX[0,destStart],destSize];
destLen ← MIN[MAX[0,destLen],destSize-destStart];
IF destLen=0 AND sourceLen=0 THEN RETURN [destStart,0];
[resultStart,resultLen] ← DoReplace[root,dest,destStart,destLen,destSize,
sourceRope,sourceRuns,sourceStart,sourceLen,event] };
 
 
ReplaceText: 
PUBLIC 
PROC [
destRoot, sourceRoot: Ref,
dest: RefTextNode, destStart: Offset ← 0, destLen: Offset ← MaxLen,
source: RefTextNode, sourceStart: Offset ← 0, sourceLen: Offset ← MaxLen,
event: Event ← NIL]
RETURNS [resultStart, resultLen: Offset] = {
-- replace the dest text by a copy of the source text
-- names that are in the replaced text move to destStart
-- names that are after the replaced text are adjusted
sourceRope: ROPE;
sourceSize, destSize: Offset;
sourceRuns: Runs;
IF dest=NIL THEN RETURN [0,0];
IF source # NIL THEN { sourceRope ← source.rope; sourceRuns ← source.runs };
sourceSize ← RopeEdit.Size[sourceRope];
sourceStart ← MIN[MAX[0,sourceStart],sourceSize];
IF sourceLen # 0 
THEN {
IF sourceStart >= sourceSize THEN sourceStart ← sourceSize;
sourceLen ← MIN[MAX[0,sourceLen], sourceSize-sourceStart]};
 
destSize ← RopeEdit.Size[dest.rope];
destStart ← MIN[MAX[0,destStart],destSize];
destLen ← MIN[MAX[0,destLen],destSize-destStart];
IF destLen=0 AND sourceLen=0 THEN RETURN [destStart,0];
[resultStart,resultLen] ← DoReplace[destRoot,dest,destStart,destLen,destSize,
sourceRope,sourceRuns,sourceStart,sourceLen,event] };
 
 
DeleteText: 
PUBLIC OneSpanProc = {
[] ← ReplaceText[root, NIL, text, start, len, NIL, 0, MaxLen, event] };
 
CopyText: 
PUBLIC DestSpanProc = { 
-- copy the specified text
[resultStart,resultLen] ← ReplaceText[destRoot, sourceRoot, dest, destLoc, 0,
source, start, len, event] };
 
 
MoveTextOnto: 
PUBLIC 
PROC [
destRoot, sourceRoot: Ref,
dest: RefTextNode, destStart: Offset ← 0, destLen: Offset ← MaxLen,
source: RefTextNode, sourceStart: Offset ← 0, sourceLen: Offset ← MaxLen,
event: Event ← NIL]
RETURNS [resultStart, resultLen: Offset] = {
sourceSize, destSize, start, len, end, destEnd: Offset;
start ← sourceStart; len ← sourceLen;
sourceSize ← Size[source];
start ← MIN[MAX[0,start],sourceSize];
len ← MIN[MAX[0,len],sourceSize-start];
end ← start+len;
destSize ← IF dest=source THEN sourceSize ELSE Size[dest];
destStart ← MIN[MAX[0,destStart],destSize];
destLen ← MIN[MAX[0,destLen],destSize-destStart];
destEnd ← destStart+destLen;
IF source=dest 
THEN { 
-- check for overlapping or adjacent
IF start 
IN [destStart..destEnd) 
THEN {
IF end < destEnd 
THEN
[] ← DeleteText[sourceRoot,source,end,destEnd-end,event];
 
IF start > destStart 
THEN
[] ← DeleteText[sourceRoot,source,destStart,start-destStart,event];
 
RETURN [destStart,len] };
 
IF end 
IN (destStart..destEnd) 
THEN {
[] ← DeleteText[sourceRoot,source,end,destEnd-end,event];
RETURN [start,len] };
 
IF start <= destStart AND end >= destEnd THEN RETURN [start,len];
IF end=destStart 
THEN {
[] ← DeleteText[sourceRoot,source,destStart,destLen,event];
RETURN [start,len] };
 
IF start=destEnd 
THEN {
[] ← DeleteText[sourceRoot,source,destStart,destLen,event];
RETURN [destStart,len] }};
 
 
[] ← DeleteText[destRoot,dest,destStart,destLen,event];
IF source=dest AND start > destStart THEN start ← start-destLen;
[resultStart,resultLen] ←
MoveText[destRoot,sourceRoot,dest,destStart,source,start,len,event] };
 
 
ReplaceByRope: 
PUBLIC 
PROC [
root: Ref, dest: RefTextNode, rope: ROPE,
start: Offset ← 0, len: Offset ← MaxLen,
inherit: BOOLEAN ← TRUE, looks: Looks ← noLooks, event: Event]
RETURNS [resultStart, resultLen: Offset] = {
notify: REF ChangingText Change;
destRope, newRope: ROPE;
alreadysaved: BOOLEAN;
destSize, ropeSize: Offset;
IF dest=NIL THEN RETURN [0,0];
ropeSize ← RopeEdit.Size[rope];
destSize ← RopeEdit.Size[destRope ← dest.rope];
start ← MIN[MAX[0,start],destSize];
len ← MIN[MAX[0,len],destSize-start];
notify ← 
IF (alreadysaved ← AlreadySaved[dest,event]) 
THEN Alloc[]
ELSE TextNode.pZone.NEW[ChangingText Change];
 
notify^ ← [ChangingText[root,dest,start,ropeSize,len,destRope,dest.runs]];
Notify[notify,before];
newRope ← RopeEdit.Replace[destRope,start,len,rope,destSize,ropeSize]; 
dest.runs ← TextLooks.ReplaceByRun[dest.runs,start,len,ropeSize,destSize,inherit,looks];
dest.rope ← newRope;
NodeAddrs.Replace[dest,start,len,ropeSize];
BumpCount[dest];
Notify[notify,after];
IF alreadysaved THEN Free[notify]
ELSE UndoEvent.Note[event,UndoChangeText,notify];
RETURN [start,ropeSize] };
 
-- **** Operation to add or delete looks ****
ChangeLooks: 
PUBLIC 
PROC [
root: Ref, text: RefTextNode, remove, add: Looks,
start: Offset ← 0, len: Offset ← MaxOffset, event: Event ← NIL] = {
-- first remove then add in the given range
size: Offset;
newRuns: Runs;
IF text=NIL THEN RETURN;
size ← RopeEdit.Size[text.rope];
start ← MIN[MAX[0,start],size];
len ← MIN[MAX[0,len],size-start];
IF len=0 THEN RETURN;
newRuns ← TextLooks.ChangeLooks[text.runs,size,remove,add,start,len]; 
IF newRuns=text.runs THEN RETURN; -- no change
DoChangeLooks[root,text,newRuns,start,len,event] };
 
DoChangeLooks: 
PROC [root: Ref, text: RefTextNode,
newRuns: Runs, start, len: Offset, event: Event] = {
oldRuns: Runs ← text.runs;
notify: REF ChangingText Change;
alreadysaved: BOOLEAN;
notify ← 
IF (alreadysaved ← AlreadySaved[text,event]) 
THEN Alloc[]
ELSE TextNode.pZone.NEW[ChangingText Change];
 
notify^ ← [ChangingText[root,text,start,len,len,text.rope,oldRuns]];
Notify[notify,before];
text.runs ← newRuns;
BumpCount[text];
Notify[notify,after];
IF alreadysaved THEN Free[notify]
ELSE UndoEvent.Note[event,UndoChangeText,notify] };
 
-- **** Changing Style / Type of node ****
ChangeType: 
PUBLIC 
PROC [node: Ref, typeName: TextNode.TypeName, event: Event ← 
NIL, root: Ref ← 
NIL] = {
notify: REF ChangingType Change;
oldTypeName: TextNode.TypeName ← node.typename;
IF node=NIL THEN RETURN;
IF root=NIL THEN root ← TextNode.Root[node];
IF oldTypeName = typeName THEN RETURN; -- no change
notify ← TextNode.pZone.
NEW[ChangingType Change ←
[ChangingType[root,node,typeName,oldTypeName]]];
 
Notify[notify,before];
node.typename ← typeName;  
Notify[notify,after];
IF event#NIL THEN UndoEvent.Note[event,UndoChangeType,notify] };
 
UndoChangeType: 
PROC [undoRef: 
REF, currentEvent: Event] = 
TRUSTED {
saved: REF Change ← NARROW[undoRef];
WITH x:saved SELECT FROM
ChangingType => ChangeType[x.node,x.oldTypeName,currentEvent,x.root];
ENDCASE => ERROR };
 
prefixAtom: ATOM = npI.PrefixAtom[];
prefixName: ROPE = npI.PrefixName[];
ChangeStyle: 
PUBLIC 
PROC [node: Ref, name: 
ROPE, event: Event ← 
NIL, root: Ref ← 
NIL] = {
newprefix: ROPE;
IF node=NIL THEN RETURN;
IF RopeEdit.Size[name] > 0 
THEN 
newprefix ← RopeEdit.Concat["\"",RopeEdit.Concat[name,"\" style"]];
 
PutProp[node,prefixName,newprefix,event,root] };
 
PutProp: 
PUBLIC 
PROC [node: Ref, name: 
ROPE, value: 
REF, event: Event ← 
NIL, root: Ref ← 
NIL] = {
notify: REF ChangingProp Change;
key: ATOM;
oldval: REF;
IF node=NIL THEN RETURN;
IF root=NIL THEN root ← TextNode.Root[node];
key ← Atom.MakeAtom[name];
IF (oldval ← npI.GetProp[node,key]) = value THEN RETURN;
notify ← TextNode.pZone.
NEW[ChangingProp Change ←
[ChangingProp[root,node,name,key,value,oldval]]];
 
Notify[notify,before];
npI.PutProp[node,key,value];  
Notify[notify,after];
IF event#NIL THEN UndoEvent.Note[event,UndoPutProp,notify] };
 
UndoPutProp: 
PROC [undoRef: 
REF, currentEvent: Event] = 
TRUSTED {
saved: REF Change ← NARROW[undoRef];
WITH x:saved SELECT FROM
ChangingProp => PutProp[x.node,x.propName,x.oldval,currentEvent,x.root];
ENDCASE => ERROR };
 
GetProp: 
PUBLIC 
PROC [node: Ref, name: 
ROPE] 
RETURNS [value: 
REF] = {
key: ATOM;
IF node=NIL THEN RETURN;
key ← Atom.MakeAtom[name];
RETURN [npI.GetProp[node,key]] };
 
-- **** Fetch info operations ****
Fetch: 
PUBLIC 
PROC [text: RefTextNode, index: Offset] 
RETURNS [
CHAR, Looks] = {
-- fetches the indexed information
-- use readers if want info from contiquous locations
RETURN [RopeEdit.Fetch[text.rope,index],
TextLooks.FetchLooks[text.runs,index]] };
 
 
FetchChar: 
PUBLIC 
PROC [text: RefTextNode, index: Offset] 
RETURNS [
CHAR] = {
-- fetches the indexed information
-- use readers if want info from contiquous locations
RETURN [RopeEdit.Fetch[text.rope,index]] };
 
FetchLooks: 
PUBLIC 
PROC
[text: RefTextNode, index: Offset] 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]] };
 
GetRope: 
PUBLIC 
PROC [text: RefTextNode] 
RETURNS [
ROPE] =
{ RETURN [IF text=NIL THEN NIL ELSE text.rope] };
 
GetRuns: 
PUBLIC 
PROC [text: RefTextNode] 
RETURNS [Runs] =
{ RETURN [IF text=NIL THEN NIL ELSE text.runs] };
 
Size: 
PUBLIC 
PROC [text: RefTextNode] 
RETURNS [Offset] =
{ RETURN [IF text=NIL THEN 0 ELSE RopeEdit.Size[text.rope]] };
 
-- **** Operations to create a text node ****
FromRope: 
PUBLIC 
PROC [rope: 
ROPE] 
RETURNS [new: RefTextNode] = {
-- create a text node with looks from a normal rope
new ← TextNode.NewTextNode[];
new.rope ← rope; new.last ← TRUE;
new.runs ← TextLooks.CreateRun[RopeEdit.Size[rope]] };
 
FromString: 
PUBLIC 
PROC [string: 
REF 
READONLY 
TEXT]
RETURNS [new: RefTextNode] = {
-- copies the contents of the string
new ← TextNode.NewTextNode[]; new.last ← TRUE;
new.rope ← RopeFrom.String[string];
new.runs ← TextLooks.CreateRun[RopeEdit.Size[new.rope]] };
 
DocFromNode: 
PUBLIC 
PROC [child: Ref] 
RETURNS [new: Ref] = {
new ← TextNode.NewTextNode[];
new.child ← child; new.last ← TRUE;
child.next ← new; child.last ← TRUE };
 
-- ***** Cap's and Lowercase
ChangeCaps: 
PUBLIC 
PROC [
root: Ref,
dest: RefTextNode, start: Offset ← 0, len: Offset ← MaxLen,
how: CapChange ← allCaps, event: Event ← NIL] = {
notify: REF ChangingText Change;
oldRope, destRope: ROPE;
rdr: RopeReader.Ref;
initCap: BOOLEAN ← TRUE;
destSize: Offset;
alreadysaved: BOOLEAN;
IF dest=NIL THEN RETURN;
destSize ← RopeEdit.Size[oldRope ← destRope ← dest.rope];
start ← MIN[MAX[0,start],destSize];
IF (len ← MIN[MAX[0,len],destSize-start])=0 THEN RETURN;
notify ← 
IF (alreadysaved ← AlreadySaved[dest,event]) 
THEN Alloc[]
ELSE TextNode.pZone.NEW[ChangingText Change];
 
notify^ ← [ChangingText[root,dest,start,len,len,destRope,dest.runs]];
Notify[notify,before];
rdr ← RopeReader.GetRopeReader[];
UNTIL len=0 
DO
c: CHAR;
maxText: NAT = 1000;
num: NAT ← MIN[len, maxText];
new: REF TEXT ← TextNode.pZone.NEW[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: BOOLEAN ← TRUE;
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.ReplaceByString[destRope,new,0,num,start,num,destSize];
IF RopeEdit.Size[destRope] # destSize THEN ERROR;
len ← len-num; start ← start+num;
ENDLOOP;
 
dest.rope ← destRope;
RopeReader.FreeRopeReader[rdr];
Notify[notify,after];
IF alreadysaved THEN Free[notify]
ELSE UndoEvent.Note[event,UndoChangeText,notify] };
 
-- ***** Miscellaneous
BumpCount: 
PROC [text: RefTextNode] = {
countLimit: Offset ← 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 };
 
 
maxRopeDepth: Offset;
debug: BOOL ← FALSE;
Flatten: 
PUBLIC 
PROC [text: RefTextNode] 
RETURNS [
BOOLEAN] = {
ropeSize, ropePieces, ropeDepth: Offset;
looksSize, looksPieces, looksDepth: Offset;
ropeFlag, looksFlag: BOOLEAN;
rope: ROPE ← text.rope;
runs: Runs ← text.runs;
[ropeSize,ropePieces,ropeDepth] ← RopeEdit.RopeStats[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];
IF ropeDepth > maxRopeDepth THEN maxRopeDepth ← ropeDepth; -- for debugging
ropeFlag ← (ropePieces > 20 
AND ropeSize < 5*ropePieces) 
OR ropeDepth > 100 
OR
(
SELECT ropePieces/8 
FROM
< 2 => ropeDepth/4 > ropePieces,
< 6 => ropeDepth/2 > ropePieces,
< 12 => ropeDepth > ropePieces,
ENDCASE => ropeDepth > ropePieces/2);
 
 
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 RopeEdit.SemiFlatten[rope]
ELSE RopeEdit.Flatten[rope];
 
text.runs ← TextLooks.Flatten[runs];
IF debug 
THEN {
CheckNode.CheckRope[text.rope];
CheckNode.CheckRuns[text.runs] };
 
RETURN [TRUE] };
 
RETURN [FALSE] };
 
AlreadySaved: 
PUBLIC 
PROC [text: RefTextNode, event: Event] 
RETURNS [
BOOLEAN] = 
TRUSTED {
-- returns TRUE if there is already a ChangingText record for given node in the event 
IF event = NIL THEN RETURN [TRUE];
FOR l: UndoEvent.SubEvent ← event.subevents, l.next 
UNTIL l=
NIL 
DO
IF l.undoRef # 
NIL 
THEN
WITH x: l.undoRef 
SELECT 
FROM
ChangingText => IF x.text = text THEN RETURN[TRUE];
ENDCASE;
 
 
ENDLOOP;
 
RETURN [FALSE] };
 
-- ***** Initialization
Start: 
PUBLIC 
PROC = {
};
 
END.