TextEditImpl.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 23, 1986 5:25:09 pm PDT
DIRECTORY
Atom USING [GetPName],
Rope USING [Balance, Cat, Concat, Fetch, Flatten, FromChar, FromProc, 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],
TextLooks USING [ChangeLooks, Concat, CreateRun, FetchLooks, Flatten, Looks, LooksStats, Replace, ReplaceByRun, Runs, Substr],
Tioga,
TiogaPrivate;
TextEditImpl: CEDAR MONITOR
IMPORTS Atom, Rope, RopeEdit, RopeReader, Rosary, TextLooks, Tioga, TiogaPrivate
EXPORTS Tioga, TiogaPrivate
~ BEGIN OPEN TiogaPrivate, Tioga;
ROPE: TYPE ~ Rope.ROPE;
Looks: TYPE ~ TextLooks.Looks;
Runs: TYPE ~ TextLooks.Runs;
ROSARY: TYPE ~ Rosary.ROSARY;
World: TYPE ~ TiogaPrivate.World;
WorldRep: PUBLIC TYPE ~ TiogaPrivate.WorldRep;
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;
};
Operations to create a text node
RopeFromString: PUBLIC PROC [string: REF READONLY TEXT, start: NAT, len: NAT] RETURNS [ROPE] ~ {
size: NAT ~ string.length;
start ← MIN[start, size];
len ← MIN[len, size-start];
IF len=0 THEN RETURN [NIL]
ELSE {
i: NAT ← start;
p: PROC RETURNS [CHAR] ~ { c: CHAR ~ string.text[i]; i ← i+1; RETURN[c] };
RETURN [Rope.FromProc[len, p]];
};
};
NodeFromRope: PUBLIC PROC [rope: ROPE, looks: Looks] RETURNS [new: Node] = {
-- create a text node with looks from a normal rope
new ← NewNode[];
new.rope ← rope;
new.runs ← TextLooks.CreateRun[Rope.Size[new.rope], looks];
};
DocFromNode: PUBLIC PROC [child: Node] RETURNS [new: Node] = {
new ← NewNode[];
new.child ← child;
child.next ← new; child.last ← TRUE;
};
Fetch info operations
Size: PUBLIC PROC [node: Node] RETURNS [INT] = {
RETURN [IF node=NIL THEN 0 ELSE Rope.InlineSize[node.rope]];
};
Fetch: PUBLIC PROC [node: Node, index: INT] RETURNS [char: XChar, looks: Looks] = {
char ← FetchChar[node, index];
looks ← FetchLooks[node, index];
};
FetchChar: PUBLIC PROC [node: Node, index: INT] RETURNS [char: XChar ← [0, 0]] = {
char.code ← ORD[Rope.Fetch[node.rope, index]];
IF node.hascharsets THEN {
charSets: ROSARY ~ GetCharSets[node];
IF charSets#NIL THEN WITH Rosary.Fetch[charSets, index] SELECT FROM
refCharSet: REF CharSet => char.set ← refCharSet^;
ENDCASE;
};
};
FetchLooks: PUBLIC PROC [node: 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[node.runs, index]];
};
GetRope: PUBLIC PROC [node: 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];
};
GetFormat: PUBLIC PROC [node: Node] RETURNS [ATOM] ~ {
RETURN[node.formatName];
};
GetComment: PUBLIC PROC [node: Node] RETURNS [BOOL] ~ {
RETURN[node.comment];
};
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];
};
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];
};
GetRosary: PROC [node: Node, name: ATOM] RETURNS [ROSARY] ~ {
WITH 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];
};
DoReplace: PROC [world: World, root: Node, dest: Text, sourceRope: ROPE, sourceRuns: Runs, sourceCharProps: ROSARY, sourceCharSets: ROSARY, sourceStart, sourceLen: INT] 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[world, dest.node];
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[world, notify, before];
IF sourceLen > 0 THEN {
replaceRope ← Rope.Substr[sourceRope, sourceStart, sourceLen];
replaceRuns ← TextLooks.Substr[sourceRuns, sourceStart, sourceLen];
};
IF dest.len=0 THEN { -- doing an insert
IF dest.start=0 THEN { -- insert at start
newRope ← Rope.Concat[replaceRope, destRope];
newRuns ← TextLooks.Concat[replaceRuns, destRuns, sourceLen, destSize];
special ← TRUE;
}
ELSE IF dest.start=destSize THEN { -- insert at end
newRope ← Rope.Concat[destRope, replaceRope];
newRuns ← TextLooks.Concat[destRuns, replaceRuns, destSize, sourceLen];
special ← TRUE;
};
}
ELSE IF sourceLen=0 THEN { -- doing a delete
IF dest.start=0 THEN { -- delete from start
newRope ← Rope.Substr[destRope, dest.len, size];
newRuns ← TextLooks.Substr[destRuns, dest.len, size];
special ← TRUE;
}
ELSE IF dest.end=destSize THEN { -- delete from end
newRope ← Rope.Substr[destRope, 0, size];
newRuns ← TextLooks.Substr[destRuns, 0, size];
special ← TRUE;
};
};
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 dest.node.hascharsets OR sourceCharSets#NIL THEN {
newCharSets: ROSARY ~ RosaryReplace[dest: destCharSets, destSize: destSize, destStart: dest.start, destLen: dest.len, source: sourceCharSets, sourceStart: sourceStart, sourceLen: sourceLen];
PutProp[dest.node, $CharSets, newCharSets];
};
IF dest.node.hascharprops OR sourceCharProps#NIL THEN {
newCharProps: ROSARY ~ RosaryReplace[dest: destCharProps, destSize: destSize, destStart: dest.start, destLen: dest.len, source: sourceCharProps, sourceStart: sourceStart, sourceLen: sourceLen];
PutProp[dest.node, $CharProps, newCharProps];
};
AddrReplace[dest.node, dest.start, dest.len, sourceLen];
BumpCount[dest.node];
EditNotify[world, notify, after];
IF alreadysaved THEN Free[notify]
ELSE NoteEvent[world, UndoChangeText, notify];
RETURN [[dest.node, dest.start, sourceLen]];
};
UndoChangeText: PROC [world: World, undoRef: REF Change] = {
x: REF Change.ChangingText ~ NARROW[undoRef];
must replace all of text since only save first change per event
[] ← DoReplace[
world: world,
root: x.root,
dest: [x.text, 0, Size[x.text]],
sourceRope: x.oldRope,
sourceRuns: x.oldRuns,
sourceCharProps: x.oldCharProps,
sourceCharSets: x.oldCharSets,
sourceStart: 0,
sourceLen: Rope.Size[x.oldRope]
];
};
ReplaceText: PUBLIC PROC [world: World, destRoot, sourceRoot: Node, dest: Text, source: Text] 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 {
IF dest.len#0 THEN DeleteText[world, destRoot, dest];
RETURN [[dest.node, dest.start, 0]];
}
ELSE RETURN DoReplace[
world: world,
root: destRoot,
dest: dest,
sourceRope: source.node.rope,
sourceRuns: source.node.runs,
sourceCharProps: GetCharProps[source.node],
sourceCharSets: GetCharSets[source.node],
sourceStart: source.start,
sourceLen: source.len
];
};
DeleteText: PUBLIC PROC [world: World, root: Node, text: Text] = {
[] ← ReplaceByRope[world: world, root: root, dest: text, rope: NIL, inherit: FALSE, looks: ALL[FALSE], charSet: 0];
};
CopyText: PUBLIC PROC [world: World, destRoot, sourceRoot: Node, dest: Location, source: Text] RETURNS [result: Text] = {
RETURN ReplaceText[world: world, destRoot: destRoot, sourceRoot: sourceRoot, dest: [dest.node, dest.where, 0], source: source];
};
MoveText: PUBLIC PROC [world: World, destRoot, sourceRoot: Node, dest: Location, source: Text] 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
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.
[] ← MapTextAddrs[dest.node, MoveAndPinAddrs];
result ← CopyText[world, destRoot, sourceRoot, dest, source];
IF dest.where<source.start THEN source.start ← source.start+source.len
Move toward start of node: CopyText has shifted the source to the right.
ELSE result.start ← dest.where-source.len;
Move toward end of node: DeleteText will shift the result to the left.
DeleteText[world, sourceRoot, source];
UnpinAllAddrs[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;
MoveTextAddr[from: source.node, to: dest.node, addr: addr, location: new];
};
};
[] ← MapTextAddrs[source.node, MoveAddrs];
result ← CopyText[world, destRoot, sourceRoot, dest, source];
DeleteText[world, sourceRoot, source];
};
};
TransposeText: PUBLIC PROC [world: World, alphaRoot, betaRoot: Node, alpha: Text, beta: Text] 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 beta.start<alpha.start OR (beta.start=alpha.start AND beta.end<alpha.end) THEN {
{ temp: Text ~ alpha; alpha ← beta; beta ← temp; };
switched ← TRUE;
};
IF alpha.end=beta.start THEN { -- turn into a move instead
betaResult ← MoveText[world, root, root, [node, alpha.start], beta];
alphaResult ← [node, alpha.start+beta.len, alpha.len];
}
ELSE IF beta.start<alpha.end THEN { -- overlapping
IF beta.end<alpha.end THEN { alphaResult ← alpha; betaResult ← 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[world, root, root, alphaHead, betaTail];
alphaResult ← [node, alpha.start+betaTail.len, alpha.len];
betaResult ← [node, alpha.start, beta.len];
};
}
ELSE { -- disjoint, alpha before beta
alphaResult ← MoveText[world, root, root, [node, beta.end], alpha];
beta.start ← beta.start-alpha.len; -- moving alpha behind beta shifts beta left
betaResult ← MoveText[world, root, root, [node, alpha.start], beta];
};
}
ELSE { -- transpose between two different nodes
alphaResult ← MoveText[world, betaRoot, alphaRoot, [beta.node, beta.start], alpha];
beta.start ← beta.start+alpha.len; -- moving alpha in front of beta shifts beta right
betaResult ← MoveText[world, alphaRoot, betaRoot, [alpha.node, alpha.start], beta];
};
IF switched THEN RETURN[betaResult, alphaResult];
};
MoveTextOnto: PUBLIC PROC [world: World, destRoot, sourceRoot: Node, dest, source: Text] 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[world, root, [node, afterStart, afterLen]];
};
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[world, root, [node, beforeStart, beforeLen]];
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[world, root, root, [node, dest.start], source];
}
ELSE { -- different nodes
DeleteText[world, destRoot, dest];
result ← MoveText[world, destRoot, sourceRoot, [dest.node, dest.start], source];
};
};
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 [world: World, root: Node, dest: Text, rope: ROPE, inherit: BOOL, looks: Looks, charSet: CharSet] 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[world, dest.node];
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[world, 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];
AddrReplace[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];
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];
PutProp[dest.node, $CharProps, newCharProps];
};
EditNotify[world, notify, after];
IF alreadysaved THEN Free[notify]
ELSE NoteEvent[world, UndoChangeText, notify];
};
RETURN [[dest.node, dest.start, ropeSize]];
};
ReplaceByChar: PUBLIC PROC [world: World, root: Node, dest: Text, char: CHAR,
inherit: BOOLTRUE, looks: Looks ← noLooks, charSet: CharSet ← 0
] RETURNS [result: Text] = {
RETURN ReplaceByRope[world: world, root: root, dest: dest, rope: Rope.FromChar[char], inherit: inherit, looks: looks, charSet: charSet];
};
Operation to add or delete looks
ChangeTextLooks: PUBLIC PROC [world: World, root: Node, text: Text, remove, add: Looks] = {
-- 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[world, text.node];
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[world, notify, before];
text.node.runs ← newRuns;
BumpCount[text.node];
EditNotify[world, notify, after];
IF alreadysaved THEN Free[notify]
ELSE NoteEvent[world, UndoChangeText, notify];
};
};
};
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 PropList. Note that we do not use Atom.PropList, 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.
PropListGet: PUBLIC PROC [propList: PropList, name: ATOM] RETURNS [value: REF] ~ {
FOR list: PropList ← propList, list.rest UNTIL list=NIL DO
prop: Prop ~ list.first;
IF prop.name=name THEN RETURN[prop.value];
ENDLOOP;
RETURN[NIL];
};
PropListPut: PUBLIC PROC [propList: PropList, name: ATOM, value: REF] RETURNS [PropList] ~ {
IF PropListGet[propList, name]=value THEN RETURN [propList]
ELSE {
rem: PropList ~ PropListRem[propList, name];
IF value=NIL THEN RETURN [rem]
ELSE RETURN [CONS[NEW[PropRep ← [name, value]], rem]];
};
};
PropListRem: PROC [propList: PropList, name: ATOM] RETURNS [PropList] ~ {
IF propList=NIL THEN RETURN [NIL]
ELSE IF propList.first.name=name THEN RETURN [propList.rest]
ELSE {
remRest: PropList ~ PropListRem[propList.rest, name];
IF propList.rest=remRest THEN RETURN [propList]
ELSE RETURN [CONS[propList.first, remRest]];
};
};
PropListMap: PUBLIC PROC [propList: PropList, action: MapPropsAction] RETURNS [quit: BOOL] ~ {
FOR list: PropList ← propList, list.rest UNTIL list=NIL DO
prop: Prop ~ list.first;
IF action[prop.name, prop.value] THEN RETURN[TRUE];
ENDLOOP;
RETURN [FALSE];
};
GetCharProp: PUBLIC PROC [node: Node, index: INT, name: ATOM] RETURNS [value: REF] ~ {
RETURN [PropListGet[GetCharPropList[node, index], name]];
};
SetCharProp: PUBLIC PROC [world: World, text: Text, name: ATOM, value: REF, root: Node] ~ {
size: INT ~ Size[text.node];
text ← ClipText[text, size];
IF text.len#0 THEN {
oldCharProps: ROSARY ~ GetCharProps[text.node];
p: PROC[q: PROC[REF, INT]] ~ {
action: Rosary.RunActionType ~ {
oldList: PropList ~ NARROW[item];
q[PropListPut[oldList, name, value], repeat];
};
RosaryMapRuns[oldCharProps, text.start, text.len, action];
};
replaceCharProps: ROSARY ~ Rosary.FromProcProc[p];
newCharProps: ROSARY ~ RosaryReplace[dest: oldCharProps, destSize: size, destStart: text.start, destLen: text.len, source: replaceCharProps, sourceStart: 0, sourceLen: text.len];
SetProp[world, text.node, $CharProps, newCharProps, 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]]];
};
SetCharPropList: PUBLIC PROC [world: World, text: Text, propList: PropList, root: Node] ~ {
size: INT ~ Size[text.node];
text ← ClipText[text, size];
IF text.len#0 THEN {
oldCharProps: ROSARY ~ GetCharProps[text.node];
replaceCharProps: ROSARY ~ Rosary.FromItem[propList, text.len];
newCharProps: ROSARY ~ RosaryReplace[dest: oldCharProps, destSize: size, destStart: text.start, destLen: text.len, source: replaceCharProps, sourceStart: 0, sourceLen: text.len];
SetProp[world, text.node, $CharProps, newCharProps, root];
};
};
MapCharProps: PUBLIC PROC [node: Node, index: INT, action: MapPropsAction] RETURNS [quit: BOOL] ~ {
RETURN PropListMap[GetCharPropList[node, index], action];
};
ModifyCharProps: PUBLIC PROC [world: World, text: Text, name: ATOM, action: ModifyPropsAction, root: Node ← NIL] RETURNS [quit: BOOL] ~ {
size: INT ~ Size[text.node];
text ← ClipText[text, size];
IF text.len#0 THEN {
prefixNIL: INT ~ INT.LAST;
prefixSize: INT ← prefixNIL;
oldCharProps: ROSARY ~ GetCharProps[text.node];
i: INT ← text.start;
p: PROC[q: PROC[REF, INT]] ~ {
runAction: Rosary.RunActionType ~ {
oldList: PropList ~ NARROW[item];
oldValue: REF ~ PropListGet[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 PropListPut[oldList, name, newValue];
q[newList, repeat];
};
i ← i + repeat;
IF stop THEN RETURN [TRUE];
};
RosaryMapRuns[oldCharProps, text.start, text.len, 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, destStart: prefixSize, destLen: replaceSize, source: replaceCharProps, sourceStart: 0, sourceLen: replaceSize];
IF Rosary.Size[replaceCharProps] # replaceSize THEN ERROR;
SetProp[world, text.node, $CharProps, newCharProps, root];
};
};
};
Changing Style / Format of node
SetFormat: PUBLIC PROC [world: World, node: Node, formatName: ATOM, root: Node ← NIL] = {
oldFormatName: ATOM ~ node.formatName;
IF root=NIL THEN root ← Root[node];
IF oldFormatName#formatName THEN {
notify: REF Change.ChangingFormat ~ NEW[Change.ChangingFormat ←
[ChangingFormat[root, node, formatName, oldFormatName]]];
EditNotify[world, notify, before];
node.formatName ← formatName;
EditNotify[world, notify, after];
NoteEvent[world, UndoSetFormat, notify];
};
};
UndoSetFormat: PROC [world: World, undoRef: REF Change] = {
x: REF Change.ChangingFormat ~ NARROW[undoRef];
SetFormat[world, x.node, x.oldFormatName, x.root];
};
quote: ROPE ~ "\"";
ChangeStyle: PUBLIC PROC [world: World, node: Node, name: ROPE, root: Node ← NIL] = {
newprefix: ROPENIL;
IF node=NIL THEN RETURN;
IF Rope.Size[name] > 0 THEN newprefix ← Rope.Cat[quote, name, quote, " style"];
SetProp[world, node, $Prefix, newprefix, root];
};
SetProp: PUBLIC PROC [world: World, node: Node, name: ATOM, value: REF, root: Node ← NIL] = {
IF node=NIL THEN NULL
ELSE {
notify: REF Change.ChangingProp;
oldval: REF ~ GetProp[node, name];
IF oldval=value THEN RETURN;
IF root=NIL THEN root ← Root[node];
notify ← NEW[Change.ChangingProp ← [ChangingProp[root, node, Atom.GetPName[name], name, value, oldval]]];
EditNotify[world, notify, before];
PutProp[node, name, value];
EditNotify[world, notify, after];
NoteEvent[world, UndoSetProp, notify];
};
};
UndoSetProp: PROC [world: World, undoRef: REF Change] = {
x: REF Change.ChangingProp ~ NARROW[undoRef];
SetProp[world: world, node: x.node, name: x.propAtom, value: x.oldval, root: x.root];
};
Caps and Lowercase
ChangeTextCaps: PUBLIC PROC [world: World, root: Node, text: Text, how: CapChange] = {
oldRope: ROPE ~ dest.node.rope;
destRope: ROPE ← oldRope;
destSize: INT ~ Rope.Size[destRope];
dest: Text ~ ClipText[text, destSize];
start: INT ← dest.start;
len: INT ← dest.len;
notify: REF Change.ChangingText;
rdr: RopeReader.Ref;
initCap: BOOLTRUE;
alreadysaved: BOOL;
IF len=0 THEN RETURN;
notify ← IF (alreadysaved ← AlreadySaved[world, dest.node]) THEN Alloc[]
ELSE NEW[Change.ChangingText];
notify^ ← [ChangingText[root, dest.node, start, len, len, destRope, dest.node.runs]];
EditNotify[world, 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[world, notify, after];
IF alreadysaved THEN Free[notify]
ELSE NoteEvent[world, UndoChangeText, notify];
};
Miscellaneous
BumpCount: PROC [text: Node] = {
countLimit: INT ← 20;
count: [0..countMax];
IF text=NIL THEN RETURN;
IF (count←text.count) = 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 { CheckRope[rope]; CheckRuns[runs] };
text.rope ← IF ropeSize>256 THEN Rope.Balance[rope] ELSE Rope.Flatten[rope];
text.runs ← TextLooks.Flatten[runs];
IF debug THEN { CheckRope[text.rope]; CheckRuns[text.runs] };
RETURN [TRUE];
};
RETURN [FALSE];
};
maxSearchDepth: NAT ← 20;
AlreadySaved: PUBLIC PROC [world: World, text: Node] RETURNS [BOOL] = {
returns TRUE if there is already a ChangingText record for given node in the event
IF world=NIL THEN RETURN [TRUE]
ELSE {
event: Event ~ world.currentEvent;
k: NAT ← maxSearchDepth;
IF event = NIL THEN RETURN [TRUE];
FOR l: 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.