Last Edited by: Teitelman, May 11, 1983 2:53 pm
DIRECTORY
AMTypes USING [TV, Type, TVType, TypeClass, TVToType],
DateAndTime USING [Parse],
IO USING [BreakProc, CreateDribbleStream, CreateOutputStreamToRope, EndOf, EndOfStream, GetCedarToken, Flush, GetChar, GetOutputStreamRope, GetToken, PutChar, PutF, PutType, RIS, ROPE, rope, SkipOver, SyntaxError, STREAM, time, TokenProc, UserAborted, WhiteSpace],
List USING [NthTail],
Menus USING [ClickProc, CreateEntry, FindEntry, AppendMenuEntry],
MessageWindow USING [Blink, Clear, Append],
Process USING [Detach, MsecToTicks, Pause],
Rope USING [Cat, Concat, Equal, Fetch, Find, FromChar, IsEmpty, Length, Replace, Run, Substr, Upper],
System USING [SecondsSinceEpoch],
Time USING [Current],
TiogaOps USING [AddLooks, BackSpace, BackWord, Break, CallWithLocks, CaretAfter, CaretBefore, CaretOnly, ClearLooks, CommandProc, CompareNodeOrder, Delete, DeleteNextCharacter, FetchLooks, FindText, FindWord, FindDef, FirstChild, GetCaret, GetProp, GetRope, GetSelection, GetTextKey, GoToPreviousCharacter, InsertRope, InsertTime, IsComment, IsDirty, LastChild, LastLocWithin, Location, Lock, LockSel, Nest, Next, NextPlaceholder, Parent, Previous, PutProp, PutTextKey, Ref, RegisterAbbrevFailedProc, RegisterCommand, RestoreSelA, RestoreSelB, SaveSelA, SaveSelB, SelectMatchingBrackets, SelectNodes, SelectPoint, SetComment, SetFormat, SetLooks, SetSelection, StepForward, SubtractLooks, Unlock, UnlockSel, UnNest, ViewerDoc],
TiogaOpsDefs USING [Ref],
TiogaMenuOps USING [tiogaMenu, Load, CloseAndOpen],
TEditDocuments2Impl USING [DoOpenFile],
UserExec USING [GetNameAndPassword, Expression, EvalExpr, CreateExpr, ParseFailed, EvaluationFailed, GetExecHandle],
UserExecExtras USING [GetChangesLog, GetSessionLog],
UserProfile USING [ProfileChangedProc, CallWhenProfileChanges, Boolean, Token],
UserTerminal USING [BlinkDisplay],
ViewerClasses USING [Viewer],
ViewerTools USING [GetSelectionContents, GetSelectedViewer],
WindowManager USING [WaitCursor, UnWaitCursor]
;
NewStuffImpl: CEDAR PROGRAM
IMPORTS AMTypes, DateAndTime, IO, List, Menus, MessageWindow, Process, Rope, System, TEditDocuments2Impl, Time, TiogaMenuOps, TiogaOps, UserExec, UserExecExtras, UserProfile, UserTerminal, ViewerTools, WindowManager
= BEGIN OPEN IO;
Types
Viewer: TYPE = ViewerClasses.Viewer;
Location: TYPE = TiogaOps.Location;
TiogaRef: TYPE = TiogaOpsDefs.Ref;
Control-E expansion
Expand: PROC RETURNS [BOOL] = {
caret: Location;
r: ROPE;
i: INT;
caret ← TiogaOps.GetCaret[];
r ← TiogaOps.GetRope[caret.node];
i ← caret.where;
IF i = 0 THEN RETURN[FALSE];
WHILE i > 0 DO
char: CHARACTER = Rope.Fetch[r, i - 1];
IF -- char # '* AND -- NOT IO.TokenProc[char] = other THEN EXIT;
i ← i -1;
ENDLOOP;
TRUSTED {Process.Detach[FORK Expand0[Rope.Substr[base: r, start: i, len: caret.where - i], caret]]}; -- forked because evaluating the expression might require confirmation.
RETURN[TRUE];
};
Expand0: PROC [name: ROPE, caret: Location] = {
Expand1: PROC [root: REF] = {
IF newName # NIL THEN
{TiogaOps.BackSpace[Rope.Length[name]];
TiogaOps.InsertRope[newName];
caret ← TiogaOps.GetCaret[];
};
TiogaOps.SaveSelA[];
TiogaOps.PutProp[n: caret.node, name: $StartOfExpansion, value: expansion];
TiogaOps.PutTextKey[node: caret.node, where: caret.where - 1, key: expansion]; -- associates a sticky address with the first character in the expansion. expansion is used as the key simply because it is a unique ref. could just as easily have done a gensym.
IF Rope.Run[s1: expansion, s2: " -- "] = 4 THEN
{TiogaOps.SetLooks[looks: "ck", which: caret];
TiogaOps.InsertRope[expansion];
TiogaOps.ClearLooks[which: caret];
TiogaOps.InsertRope[" = {"];
TiogaOps.Break[];
TiogaOps.Nest[];
TiogaOps.InsertRope["Body"];
TiogaOps.Break[];
TiogaOps.InsertRope["};"];
[] ← TiogaOps.NextPlaceholder[dir: backwards, gotoend: FALSE];
}
ELSE
{TiogaOps.InsertRope[expansion];
TiogaOps.GoToPreviousCharacter[Rope.Length[expansion]];
[] ← TiogaOps.NextPlaceholder[gotoend: FALSE];
};
};
viewer: Viewer;
expansion, newName: ROPE;
[viewer, ] ← TiogaOps.GetSelection[];
IF Rope.Find[name, "."] = -1 AND UserExec.GetExecHandle[viewer: viewer] = NIL THEN
{n: TiogaRef ← TiogaOps.FirstChild[TiogaOps.ViewerDoc[viewer]]; -- should check and if module not loaded, then search file instead of evaluating
WHILE n # NIL DO
r: ROPE = TiogaOps.GetRope[n];
IF NOT TiogaOps.IsComment[n] AND (Rope.Find[r, "PROGRAM"] # -1 OR Rope.Find[r, "MONITOR"] # -1) THEN
{name ← Rope.Cat[IO.GetCedarToken[IO.RIS[r]], ".", name];
EXIT
};
n ← TiogaOps.Next[n];
ENDLOOP;
};
MessageWindow.Append[message: Rope.Cat["Evaluating ", name, "..."], clearFirst: TRUE];
WindowManager.WaitCursor[];
[expansion, newName] ← ExpandRecord[name, viewer ! UNWIND => WindowManager.UnWaitCursor[]];
WindowManager.UnWaitCursor[];
IF viewer = TiogaOps.GetSelection[].viewer THEN
{IF TiogaOps.GetCaret[] # caret THEN
{MessageWindow.Append["Caret has moved, insertion NOT performed.", TRUE];
MessageWindow.Blink[];
RETURN;
};
}
ELSE TiogaOps.SetSelection[viewer: viewer, start: caret, end: caret]; -- user went to some other viewer or exec while waiting.
MessageWindow.Clear[];
IF useLocks THEN TiogaOps.CallWithLocks[proc: Expand1]
ELSE Expand1[NIL];
};
ExpandRecord: PROC [name: ROPE, viewer: Viewer] RETURNS [expansion: ROPE, newName: ROPE] = {
h: STREAM = CreateOutputStreamToRope[];
i: INT;
expr: UserExec.Expression = UserExec.CreateExpr[name];
tv: AMTypes.TV;
type: AMTypes.Type;
[] ← UserExec.EvalExpr[expr: expr, exec: NIL, viewer: viewer !
UserExec.ParseFailed => {MessageWindow.Append[msg, TRUE]; MessageWindow.Blink[]; GOTO Fail};
UserExec.EvaluationFailed => {MessageWindow.Append[msg, TRUE]; MessageWindow.Blink[]; GOTO Fail};
IO.UserAborted => {MessageWindow.Append["Aborted", TRUE]; GOTO Fail};
];
tv ← expr.value;
type ← AMTypes.TVType[tv];
IF expr.correctionMade THEN newName ← expr.rope;
IF AMTypes.TypeClass[type] = type THEN
{h.PutType[type: AMTypes.TVToType[tv], verbose: TRUE]; -- if the tv describes a type, e.g. UserExec.CommandProc, don't want the type of that object, which would just be TYPE, but rather the object itself, printed as a type.
expansion ← h.GetOutputStreamRope[];
i ← Rope.Find[expansion, "["];
IF i = -1 THEN expansion ← NIL
ELSE expansion ← Rope.Cat[" -- ", Rope.Substr[base: expansion, start: i], " -- ", ];
RETURN;
};
h.PutType[type: type, verbose: TRUE];
expansion ← h.GetOutputStreamRope[];
IF (i ← Rope.Find[expansion, "RETURNS"]) # -1 THEN expansion ← Rope.Substr[base: expansion, len: i - 1]; -- the minus one is to delete the space before the RETURNS. important in case of typescripts, so that ctrl-next puts you at the end of the typescript.
i ← Rope.Find[expansion, "["];
IF i = -1 THEN {expansion ← "[]"; RETURN};
expansion ← Rope.Substr[expansion, i]; -- strips off the PROCEDURE, PROC, ERROR, RECORD, whatever.
{Expand1: PROC [args: ROPE] RETURNS[expansion: ROPE] = {
left: ROPE = Rope.FromChar['\001];
right: ROPE = Rope.FromChar['\002];
i, length: INT;
i ← 1;
expansion ← args;
length ← Rope.Length[expansion];
DO
-- i is the first character in this argument. find end of argument
n: INT ← -1; -- will be character position of :
j: INT ← i; -- will be position of the first character beyond this argument, i.e. the , or ]
WHILE j < length DO
SELECT Rope.Fetch[expansion, j] FROM
'', '\\ => j ← j + 2;
'], ', => EXIT;
': => n ← j;
'[ => -- default value for this argument is a record constructor. find matching right ] and replace interior by its expansion
{count: INT ← 1;
k: INT ← j + 1;
r: ROPE;
nChars: INT;
WHILE k < length DO
SELECT Rope.Fetch[expansion, k] FROM
'', '\\ => {k ← k + 2; LOOP};
'[ => count ← count + 1;
'] => IF (count ← count - 1) = 0 THEN EXIT;
ENDCASE;
k ← k + 1;
REPEAT
FINISHED => GOTO Fail; -- did not find matching ]
ENDLOOP;
nChars ← k - j + 1;
r ← Expand1[Rope.Substr[base: expansion, start: j, len: nChars]];
expansion ← Rope.Replace[base: expansion, start: j, len: nChars, with: r];
length ← Rope.Length[expansion];
j ← k + (Rope.Length[r] - nChars) + 1; -- first character beyond the matching ]
EXIT;
};
ENDCASE;
j ← j + 1;
ENDLOOP;
IF n = -1 OR j >= length THEN GOTO Fail;
expansion ← Rope.Replace[base: expansion, start: j, len: 0, with: right]; -- insert right place holder. work from right so positions accurate.
expansion ← Rope.Replace[base: expansion, start: n + 2, len: 0, with: left]; -- + 2 to skip over the : and the space.
length ← length + 2;
i ← j + 3;
IF i = length THEN EXIT;
ENDLOOP;
EXITS
Fail => expansion ← args;
};
expansion ← Expand1[expansion];
};
EXITS
Fail => expansion ← NIL;
};
Control-Next
CtrlNextPlaceholder: TiogaOps.CommandProc -- [viewer: ViewerClasses.Viewer ← NIL] RETURNS [recordAtom: BOOL ← TRUE, quit: BOOL ← FALSE] -- = {
Cleanup1: PROC [root: TiogaRef] = {
viewer: Viewer;
start, end: Location;
key: REF ANY;
fieldsRemain: BOOLFALSE;
[viewer, start, end] ← TiogaOps.GetSelection[];
IF (key ← TiogaOps.GetProp[n: start.node, name: $StartOfExpansion]) # NIL THEN {
TiogaOps.SelectPoint[viewer: viewer, caret: [start.node, TiogaOps.GetTextKey[start.node, key].where + 2]]; -- + 1 takes us to the [, +2 to the first character inside of the [. when sticky addresses done right, then can make sticky address for both start and end of selection and not worry about node breaks, and then not have to do the SelectMatchingBrackets
};
IF TiogaOps.SelectMatchingBrackets['[, ']] THEN [, start, end] ← TiogaOps.GetSelection[]
ELSE
{r: ROPE = TiogaOps.GetRope[start.node];
IF start.node = end.node AND Rope.Fetch[r, start.where] = '\001 AND Rope.Fetch[r, end.where] = '\002 THEN TiogaOps.Delete[];
[] ← TiogaOps.NextPlaceholder[gotoend: FALSE];
RETURN;
};
TiogaOps.PutTextKey[node: end.node, where: end.where, key: $EndCleanup];
TiogaOps.SaveSelA[];
TiogaOps.CaretBefore[];
DO
loc: Location;
r: ROPE;
IF NOT TiogaOps.NextPlaceholder[gotoend: FALSE].found THEN EXIT;
loc ← TiogaOps.GetCaret[];
IF loc.node # end.node OR loc.where > TiogaOps.GetTextKey[node: end.node, key: $EndCleanup].where THEN EXIT;
r ← ViewerTools.GetSelectionContents[];
IF Rope.Find[r, "←"] = -1 THEN {
fieldsRemain ← TRUE;
LOOP;
};
TiogaOps.Delete[];
loc ← TiogaOps.GetCaret[];
r ← TiogaOps.GetRope[loc.node];
IF Rope.Fetch[r, loc.where - 2] = ': THEN TiogaOps.BackWord[]; -- deletes the name:
loc ← TiogaOps.GetCaret[];
r ← TiogaOps.GetRope[loc.node];
IF Rope.Fetch[r, loc.where - 1] # '[ THEN -- not first argument, delete preceding space and comma
TiogaOps.BackSpace[2]
ELSE IF Rope.Fetch[r, loc.where] = ', THEN -- was first argument, but not last argument, delete next space and comma
TiogaOps.DeleteNextCharacter[2]
ENDLOOP;
end.where ← TiogaOps.GetTextKey[node: end.node, key: $EndCleanup].where;
IF fieldsRemain THEN {
TiogaOps.RestoreSelA[];
[] ← TiogaOps.NextPlaceholder[gotoend: FALSE];
MessageWindow.Append["Selected field cannot be defaulted.", TRUE];
}
ELSE {
TiogaOps.RestoreSelA[];
TiogaOps.CaretAfter[];
TiogaOps.CaretOnly[];
};
};
IF useLocks THEN TiogaOps.CallWithLocks[proc: Cleanup1]
ELSE Cleanup1[NIL];
};
NextPlaceholder: TiogaOps.CommandProc -- [viewer: Viewer ← NIL] RETURNS [recordAtom: BOOL ← TRUE, quit: BOOL ← FALSE] --
= {
start, end: Location;
node: TiogaRef;
r: ROPE;
IF NOT TiogaOps.NextPlaceholder[gotoend: FALSE].found THEN RETURN;
quit ← TRUE;
[, start, end, ] ← TiogaOps.GetSelection[];
IF start.where # 0 THEN RETURN;
r ← TiogaOps.GetRope[start.node];
IF NOT Rope.Equal[r, " ."] THEN RETURN;
r ← TiogaOps.GetRope[(node ← TiogaOps.Next[start.node])];
IF NOT Rope.Equal[r, " ."] THEN RETURN;
r ← TiogaOps.GetRope[(node ← TiogaOps.Next[node])];
IF NOT Rope.Equal[r, " ."] THEN RETURN;
r ← TiogaOps.GetRope[TiogaOps.Next[node]];
TiogaOps.SetSelection[viewer, start, start];
TiogaOps.Break[];
TiogaOps.InsertRope[Rope.Substr[base: r, len: Rope.Length[r] - 1]];
TiogaOps.GoToPreviousCharacter[Rope.Length[r]];
TiogaOps.CaretOnly[];
[] ← TiogaOps.NextPlaceholder[gotoend: TRUE];
};
Save operations: updating date, changelog
sessionLog, changesLog: IO.STREAM;
RedSave: TiogaOps.CommandProc = {
changeLogNode: TiogaRef;
description: ROPE;
Stars: PROCEDURE = {
sessionLog.PutChar['\n];
FOR i: INT IN [0..50) DO
sessionLog.PutChar['*];
ENDLOOP;
sessionLog.PutChar['\n];
};
LogChange: PROC [out: IO.STREAM] = {
IF sessionLog # NIL THEN Stars[];
out.PutF["\nChanged File: %g\n", rope[viewer.name]];
IF description = NIL THEN out.PutF["\tEdited on %t (no change log entry)\n", time[]]
ELSE out.PutF["%g", rope[description]];
IF sessionLog # NIL THEN Stars[];
out.Flush[];
};
IF sessionLog = NIL THEN sessionLog ← UserExecExtras.GetSessionLog[];
IF changesLog = NIL THEN {
changesLog ← UserExecExtras.GetChangesLog[];
IF changesLog # NIL AND sessionLog # NIL THEN changesLog ← IO.CreateDribbleStream[changesLog, sessionLog];
};
changeLogNode ← UpdateForSave[viewer: viewer, updateLastEdited: updateLastEdited, updateChangeLog: updateChangeLog, insertPlaceHolders: FALSE, mergeWithPrevious: ifSoonEnough];
IF changeLogNode # NIL THEN {
node: TiogaOps.Ref ← TiogaOps.FirstChild[changeLogNode];
description ← Rope.Cat["\t", TiogaOps.GetRope[changeLogNode], "\n"];
WHILE node # NIL DO
description ← Rope.Cat[description, "\t\t", TiogaOps.GetRope[node], "\n"];
 node ← TiogaOps.Next[node];
ENDLOOP;
};
IF changesLog # NIL THEN LogChange[changesLog]
ELSE IF sessionLog # NIL THEN LogChange[sessionLog];
};
UpdateForSave: PROC [viewer: Viewer, updateLastEdited: BOOL, updateChangeLog: UpdateChange, insertPlaceHolders: BOOL, mergeWithPrevious: MergeChange] RETURNS[changeLogNode: TiogaRef] = {
rootNode: TiogaRef = TiogaOps.ViewerDoc[viewer]; -- root, the argument passed in to WriteDate1, is the root of the selection. which might not be in the viewer being saved.
user: ROPE ← UserExec.GetNameAndPassword[].name;
i: INT;
IF Rope.IsEmpty[user] THEN RETURN;
IF (i ← Rope.Find[user, "."]) # -1 THEN user ← Rope.Substr[base: user, len: i];
user ← Rope.Concat[Rope.FromChar[Rope.Upper[Rope.Fetch[user, 0]]], Rope.Substr[base: user, start: 1]];
{
ENABLE UNWIND => IF useLocks THEN {TiogaOps.Unlock[rootNode]; TiogaOps.UnlockSel[]};
restoreSel: BOOLTRUE;
TiogaOps.SaveSelA[]; -- for restoration, e.g. so user can click save and then click compile and not have to be aware of the fact that save will change the selection.
IF useLocks THEN {TiogaOps.LockSel[]; TiogaOps.Lock[rootNode]};
IF updateLastEdited THEN UpdateLastEdited[viewer, rootNode, user];
IF updateChangeLog # never THEN [changeLogNode, restoreSel] ← UpdateChangeLog[viewer, rootNode, user, updateChangeLog, insertPlaceHolders, mergeWithPrevious];
IF restoreSel THEN TiogaOps.RestoreSelA[];
IF useLocks THEN {TiogaOps.Unlock[rootNode]; TiogaOps.UnlockSel[]};
};
};
FindDate: PROC [r: ROPE] RETURNS [i, j: INT] = {
FOR months: LIST OF ROPELIST["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"], months.rest UNTIL months = NIL DO
i ← Rope.Find[r, months.first];
IF i # -1 THEN
{IF (j ← Rope.Find[r, " am", i]) # -1 OR (j ← Rope.Find[r, " pm", i]) # -1 THEN
RETURN[i, j + 2]; -- returns position of first and last character in date.
};
ENDLOOP;
RETURN[-1, -1];
};
SelectDate: PROC [viewer: Viewer, loc: Location, rope: ROPE] RETURNS [BOOLEAN] = {
loc is the location corresponding to r. In most cases, where is 0 and r is GetRope. If r contains a date, this procedure selects the date and ret urns TRUE. Otherwise returns FALSE.
i, j: INT;
[i, j] ← FindDate[rope];
IF i = -1 THEN RETURN[FALSE];
TiogaOps.SetSelection[viewer: viewer, start: [node: loc.node, where: loc.where + i], end: [node: loc.node, where: loc.where + j]];
TiogaOps.SetLooks[looks: TiogaOps.FetchLooks[loc.node, loc.where + i], which: caret];
RETURN[TRUE];
};
UpdateLastEdited: PROC [viewer: Viewer, rootNode: TiogaRef, user: ROPE] = {
node: TiogaRef;
nodeRope: ROPE;
s: IO.STREAM;
current, next: INT ← 0;
r, insertRope: ROPE;
foundOne: BOOLFALSE;
insertAt: Location ← [NIL, 0];
DO
IF node = NIL THEN -- first time
{node ← TiogaOps.FirstChild[rootNode];
TiogaOps.SelectPoint[viewer: viewer, caret: [node: node, where: 0]];
nodeRope ← TiogaOps.GetRope[node];
next ← -1;
}
ELSE IF next = -1 THEN -- just exhausted a node
{node ← TiogaOps.StepForward[node];
IF node = NIL THEN EXIT;
IF insertAt.node # node AND NOT Rope.IsEmpty[nodeRope] THEN insertAt ← [node, 0];
nodeRope ← TiogaOps.GetRope[node];
current ← 0;
}
ELSE
{current ← next + 1;
IF NOT Rope.IsEmpty[r] THEN insertAt ← [node, current];
};
IF TiogaOps.IsComment[node] THEN
r ← nodeRope
ELSE
{c: CHAR;
oldR: ROPE = r;
IF (next ← Rope.Find[s1: nodeRope, s2: "\n", pos1: current]) # -1 THEN r ← Rope.Substr[base: nodeRope, start: current, len: next - current]
ELSE IF current # 0 THEN r ← Rope.Substr[base: nodeRope, start: current]
ELSE r ← nodeRope;
s ← IO.RIS[r, s];
IO.SkipOver[s, IO.WhiteSpace];
IF s.EndOf[] THEN LOOP; -- line with just white space.
c ← s.GetChar[];
IF (c = '- OR c = '/) AND NOT s.EndOf[] AND (c = s.GetChar[]) THEN NULL -- is a comment
ELSE EXIT;
};
IF Rope.IsEmpty[r] THEN LOOP;
IF Rope.Find[s1: r, s2: user, case: FALSE] # -1 AND SelectDate[viewer: viewer, loc: [node, current], rope: r] THEN
{TiogaOps.SaveSelB[]; -- modify the last one, so user can have separate created and edited comments.
foundOne ← TRUE;
};
LOOP;
ENDLOOP;
IF foundOne THEN
{TiogaOps.RestoreSelB[];
TiogaOps.Delete[];
TiogaOps.InsertTime[];
RETURN;
};
-- not found, insert new date string at top.
IF insertAt.node = NIL THEN RETURN; -- no comments seen
TiogaOps.SelectPoint[viewer: viewer, caret: insertAt];
insertRope ← Rope.Cat["Last Edited by: ", user, ", "];
IF current # 0 THEN -- inserting inside of a node, or no node structure
{TiogaOps.InsertRope[Rope.Cat["-- ", insertRope]];
TiogaOps.InsertTime[];
TiogaOps.InsertRope["\n"];
}
ELSE
{IF NOT Rope.IsEmpty[TiogaOps.GetRope[insertAt.node]] THEN -- inserting in front of a non-comment node, e.g. the DIRECTORY.
TiogaOps.Break[];
TiogaOps.SetFormat[format: NIL];
TiogaOps.InsertRope[insertRope];
TiogaOps.InsertTime[];
TiogaOps.SetComment[];
};
};
ChangeEntry: TYPE = RECORD[rope, path: ROPENIL, node: TiogaRef ← NIL, written: BOOLFALSE];
ChangeLogEntry: TYPE = RECORD[changed: LIST OF REF ChangeEntry, node: TiogaRef];
UpdateChangeLog: PROC [viewer: Viewer, rootNode: TiogaRef, user: ROPE, updateChangeLog: UpdateChange, insertPlaceHolders: BOOLFALSE, mergeWithPrevious: MergeChange] RETURNS [changeLogNode: TiogaRef ← NIL, restoreSel: BOOLTRUE] = {
end: Location = TiogaOps.LastLocWithin[TiogaOps.LastChild[rootNode]];
changed, tailOfChanged, lag: LIST OF REF ChangeEntry ← NIL;
s: IO.STREAM;
currentChangeLogEntry: REF ChangeLogEntry ← NARROW[TiogaOps.GetProp[n: rootNode, name: $ChangeLog]];
previousEntryNode: TiogaRef;
first: BOOLTRUE;
writeAll: BOOLTRUE;
anyChanges: BOOLFALSE;
CreateChangeEntry: PROC [node: TiogaRef] = {
change: REF ChangeEntry;
node0, node1: TiogaRef;
entriesToBeMerged: BOOLFALSE;
IsADecl: PROCEDURE [node: TiogaRef] RETURNS[declName: ROPENIL] = {
r, name: ROPE;
r ← TiogaOps.GetRope[node];
IF r.IsEmpty[] THEN RETURN[NIL];
s ← IO.RIS[r, s];
{ENABLE {
IO.EndOfStream => CONTINUE;
IO.SyntaxError => CONTINUE;
};
name ← IO.GetCedarToken[s];
IF Rope.Equal[IO.GetCedarToken[s], ":"] AND (Rope.Find[r, "{"] # -1 OR Rope.Find[r, "BEGIN"] # -1) THEN RETURN[name];
};
};
AlreadySeen: PROCEDURE [node: TiogaRef] RETURNS [BOOL] = {
FOR l: LIST OF REF ChangeEntry ← changed, l.rest UNTIL l = NIL DO
IF l.first.node = NIL THEN entriesToBeMerged ← TRUE
ELSE IF l.first.node = node THEN RETURN[TRUE];
ENDLOOP;
RETURN[FALSE];
};
depth: INT ← 0;
node1 ← node;
UNTIL node1 = rootNode DO
name: ROPE;
IF TiogaOps.IsComment[node1] THEN NULL
ELSE IF change = NIL AND AlreadySeen[node1] THEN RETURN
ELSE IF (name ← IsADecl[node1]) = NIL THEN node0 ← node1 -- best entry so far
ELSE IF change = NIL THEN change ← NEW[ChangeEntry ← [node: node1, rope: name]]
ELSE
{IF change.path # NIL THEN change.path ← Rope.Concat[change.path, ", "];
change.path ← Rope.Cat[change.path, "local of ", name];
};
node1 ← TiogaOps.Parent[node1];
depth ← depth + 1;
ENDLOOP;
IF node0 # NIL AND change = NIL THEN
{s ← IO.RIS[TiogaOps.GetRope[node0]];
change ← NEW[ChangeEntry ← [node: node0, rope: IO.GetCedarToken[s ! IO.EndOfStream => CONTINUE]]];
};
IF change # NIL AND entriesToBeMerged THEN
FOR l: LIST OF REF ChangeEntry ← changed, l.rest UNTIL l = NIL DO
IF l.first.node = NIL AND Rope.Equal[l.first.rope, change.rope] AND Rope.Equal[l.first.path, change.path] THEN -- user has made a change corresponding to an entry from a previous change log that this change log is being merged with.
{l.first.node ← change.node;
change ← NIL;
EXIT;
};
ENDLOOP;
IF change # NIL THEN
{IF changed = NIL THEN
{changed ← LIST[change];
tailOfChanged ← changed;
}
ELSE
{tailOfChanged.rest ← LIST[change];
tailOfChanged ← tailOfChanged.rest;
};
};
};
IsDirty: PROC [node: TiogaRef] = {
child: TiogaRef;
IF TiogaOps.IsDirty[node] THEN CreateChangeEntry[node]; -- note that must still look at children, because they may require a separate entry, e.g. in the case that there are changes to local procedures. Or, this node being a comment might not have caused an entry.
child ← TiogaOps.FirstChild[node];
UNTIL child = NIL DO
IsDirty[child];
child ← TiogaOps.Next[child];
ENDLOOP;
};
DeletePlaceHolders: PROC = {
TiogaOps.SelectPoint[viewer: viewer, caret: [currentChangeLogEntry.node, 0]];
WHILE TiogaOps.NextPlaceholder[gotoend: FALSE].found DO
n: TiogaRef;
TiogaOps.Delete[];
n ← TiogaOps.GetCaret[].node;
IF Rope.IsEmpty[TiogaOps.GetRope[n]] THEN
{TiogaOps.SelectNodes[viewer, n, n];
TiogaOps.Delete[];
};
ENDLOOP;
};
main body of UpdateChangeLog
IF mergeWithPrevious = newSession AND currentChangeLogEntry # NIL THEN -- start new session even if currently working on a session.
{currentChangeLogEntry ← NIL;
changed ← tailOfChanged ← NIL;
-- ClearBits[rootNode];
}
ELSE IF currentChangeLogEntry # NIL THEN -- see if old change log is valid, and if so, initialize changed to appropriate value.
{IF TiogaOps.CompareNodeOrder[node1: rootNode, node2: currentChangeLogEntry.node] = disjoint THEN -- node was deleted -- GOTO NoLog;
IF NOT SelectDate[viewer: viewer, loc: [currentChangeLogEntry.node, 0], rope: TiogaOps.GetRope[currentChangeLogEntry.node]] THEN GOTO NoLog;
changed ← currentChangeLogEntry.changed;
TRUSTED {tailOfChanged ← LOOPHOLE[List.NthTail[LOOPHOLE[changed], -1]]};
EXITS
NoLog => currentChangeLogEntry ← NIL;
};
IF currentChangeLogEntry = NIL THEN {
-- no active change log. Is this one of those cases where we are going to create a new one or merge with a previous one?
FindLastChangeLog: PROCEDURE RETURNS [TiogaRef ← NIL] = {
n: TiogaRef ← end.node;
WHILE n # NIL DO -- find last non-empty comment node.
r: ROPE = TiogaOps.GetRope[n];
IF Rope.IsEmpty[r] THEN {n ← TiogaOps.Previous[n, rootNode]; LOOP};
IF NOT TiogaOps.IsComment[n] OR NOT SelectDate[viewer: viewer, loc: [n, 0], rope: r] THEN RETURN;
EXIT;
REPEAT
FINISHED => RETURN;
ENDLOOP;
n ← TiogaOps.FirstChild[n];
WHILE n # NIL DO -- look at its children, see if there is a "changes to: " entry.
IF Rope.Find[TiogaOps.GetRope[n], "changes to: "] = 0 THEN RETURN[n];
n ← TiogaOps.Next[n];
REPEAT
FINISHED => RETURN;-- the last thing in the file did not look like a change log entry that was inserted by this package
ENDLOOP;
};
IfSoonEnough: PROCEDURE RETURNS [BOOL] = {
nowInSeconds, thenInSeconds: LONG CARDINAL;
then: ROPE = ViewerTools.GetSelectionContents[];
TRUSTED {nowInSeconds ← System.SecondsSinceEpoch[Time.Current[]]};
thenInSeconds ← System.SecondsSinceEpoch[DateAndTime.Parse[then].dt];
RETURN[nowInSeconds - thenInSeconds < mergeInterval];
};
SELECT updateChangeLog FROM
updateOnly => RETURN;
viaChangeLogButton => NULL; -- user clicked ChangeLog button. create one if not there
automaticallyCreateNew => -- create a new one if looks like source file.
{n: TiogaRef ← end.node;
WHILE n # rootNode DO
r: ROPE = TiogaOps.GetRope[n];
IF Rope.IsEmpty[r] OR TiogaOps.IsComment[n] THEN {n ← TiogaOps.Previous[n, rootNode]; LOOP};
IF Rope.Find[s1: r, s2: "END.", case: FALSE] # 0 THEN RETURN;
EXIT;
REPEAT
FINISHED => RETURN;
ENDLOOP;
};
addIfPrevious => -- add one if already one there of appropriate form.
IF (previousEntryNode ← FindLastChangeLog[]) = NIL THEN RETURN;
ENDCASE => ERROR;
IF mergeWithPrevious = newSession THEN previousEntryNode ← NIL
ELSE IF updateChangeLog # addIfPrevious THEN -- if = addIfPrevious then FindLastChangeLog was already called
previousEntryNode ← FindLastChangeLog[];
IF previousEntryNode # NIL AND mergeWithPrevious = ifSoonEnough AND NOT IfSoonEnough[] THEN previousEntryNode ← NIL;
};
if we get to here, we are going to write or update a change log entry
IF previousEntryNode # NIL THEN -- parse the entry.
{r: ROPE ← TiogaOps.GetRope[previousEntryNode];
change: REF ChangeEntry;
s ← IO.RIS[Rope.Substr[r, Rope.Find[r, ":"] + 1]];
WHILE (r ← IO.GetToken[s]) # NIL DO
change ← NEW[ChangeEntry ← [rope: r, written: TRUE]];
IF changed = NIL THEN
{changed ← LIST[change];
tailOfChanged ← changed;
}
ELSE
{tailOfChanged.rest ← LIST[change];
tailOfChanged ← tailOfChanged.rest;
};
IF Rope.Equal[r ← IO.GetToken[s], "("] THEN -- read "Local to"
{rparProc: IO.BreakProc = {
RETURN[IF char = ') THEN sepr ELSE other];
};
change.path ← IO.GetToken[s, rparProc];
r ← IO.GetToken[s];
};
WHILE r # NIL DO -- skip over any comments that the user may have inserted
IF Rope.Equal[r, ","] THEN EXIT;
r ← IO.GetToken[s];
ENDLOOP;
ENDLOOP;
TiogaOps.PutProp[
n: rootNode,
name: $ChangeLog,
value: currentChangeLogEntry ← NEW[ChangeLogEntry ← [changed: changed, node: TiogaOps.Parent[previousEntryNode]]]
];
};
writeAll ← (currentChangeLogEntry = NIL);
IsDirty[rootNode]; -- scan the tree structure looking for changes
lag ← NIL;
FOR l: LIST OF REF ChangeEntry ← changed, l.rest UNTIL l = NIL DO -- now see if there were any new changes
r: ROPE ← l.first.rope;
IF NOT writeAll AND l.first.written THEN NULL
ELSE IF TiogaOps.CompareNodeOrder[node1: rootNode, node2: l.first.node] = disjoint THEN
{IF lag = NIL THEN changed ← changed.rest ELSE lag.rest ← l.rest} -- remove the node
ELSE IF TiogaOps.IsComment[l.first.node] THEN ERROR -- shouldn't happen
ELSE {anyChanges ← TRUE; EXIT}; -- something found
lag ← l;
ENDLOOP;
-- tell the user what we decided to do
IF updateChangeLog = viaChangeLogButton THEN
{IF NOT anyChanges AND mergeWithPrevious = ifSoonEnough THEN MessageWindow.Append["No (new) changes found.", TRUE]
ELSE IF currentChangeLogEntry = NIL THEN
{MessageWindow.Append["Starting new edit session.", TRUE]; -- e.g. no changes, but user yellow clicked to start new session.
anyChanges ← TRUE; -- want to write out changeLog so that will know next time not to merge.
}
ELSE IF previousEntryNode # NIL THEN MessageWindow.Append["Continuing with previous edit session.", TRUE]
}
ELSE IF anyChanges THEN
MessageWindow.Append[
(IF currentChangeLogEntry = NIL THEN "Writing new change log entry."
ELSE IF previousEntryNode # NIL THEN "Merging changes with previous log entry."
ELSE "Updating current change log entry"),
TRUE
];
IF NOT anyChanges THEN -- don't write anything out
{IF currentChangeLogEntry # NIL THEN {
currentChangeLogEntry.changed ← changed;
changeLogNode ← currentChangeLogEntry.node; -- for return value
IF NOT insertPlaceHolders THEN DeletePlaceHolders[];
};
RETURN;
};
IF currentChangeLogEntry # NIL THEN -- change the date, position caret at end and only write those that have changed since last edit.
{TiogaOps.Delete[];
TiogaOps.InsertTime[];
IF insertPlaceHolders THEN
{lastChild, n: TiogaRef;
lastChild ← TiogaOps.LastChild[currentChangeLogEntry.node];
IF lastChild # NIL THEN n ← TiogaOps.Previous[lastChild];
IF n # NIL THEN
{TiogaOps.SelectPoint[viewer: viewer, caret: TiogaOps.LastLocWithin[n]];
TiogaOps.InsertRope["more comments"];
}
ELSE
{TiogaOps.SelectPoint[viewer: viewer, caret: [lastChild, 0]];
TiogaOps.Break[];
TiogaOps.InsertRope["general comments"];
};
};
TiogaOps.SelectPoint[viewer: viewer, caret: TiogaOps.LastLocWithin[currentChangeLogEntry.node]]; -- move to end.
}
ELSE -- creating a new one
{TiogaOps.SelectPoint[viewer: viewer, caret: end];
TiogaOps.PutProp[
n: rootNode,
name: $ChangeLog,
value: currentChangeLogEntry ← NEW[ChangeLogEntry ← [changed: NIL, node: TiogaOps.GetCaret[].node]]
];
IF NOT Rope.IsEmpty[TiogaOps.GetRope[end.node]] THEN TiogaOps.Break[];
TiogaOps.InsertRope["Edited on "];
TiogaOps.InsertTime[];
TiogaOps.InsertRope[Rope.Cat[", by ", user]];
TiogaOps.SetComment[];
TiogaOps.Break[];
TiogaOps.Nest[];
IF insertPlaceHolders THEN
{TiogaOps.InsertRope["general comments"];
TiogaOps.SetComment[];
TiogaOps.Break[];
};
TiogaOps.SetComment[];
TiogaOps.InsertRope["changes to: "];
};
TiogaOps.SetLooks["r", caret];
FOR l: LIST OF REF ChangeEntry ← changed, l.rest UNTIL l = NIL DO
r: ROPE ← l.first.rope;
IF NOT writeAll AND l.first.written THEN LOOP;
IF NOT Rope.IsEmpty[r] THEN
{IF NOT first OR NOT writeAll THEN TiogaOps.InsertRope[", "];
TiogaOps.InsertRope[r];
IF NOT Rope.IsEmpty[l.first.path] THEN
{TiogaOps.SubtractLooks["r", caret];
TiogaOps.InsertRope[" ("];
TiogaOps.InsertRope[l.first.path];
TiogaOps.InsertRope[")"];
TiogaOps.AddLooks["r"];
};
IF insertPlaceHolders THEN
{TiogaOps.InsertRope[""];
TiogaOps.SubtractLooks["r", caret];
TiogaOps.InsertRope[" "];
TiogaOps.AddLooks["r"];
TiogaOps.InsertRope[""];
};
first ← FALSE;
};
l.first.written ← TRUE;
ENDLOOP;
currentChangeLogEntry.changed ← changed;
changeLogNode ← currentChangeLogEntry.node; -- for return value
TiogaOps.Break[];
TiogaOps.UnNest[];
IF NOT insertPlaceHolders THEN DeletePlaceHolders[]
ELSE IF NOT first THEN -- something inserted with placeHolders
{TiogaOps.SelectPoint[viewer: viewer, caret: [currentChangeLogEntry.node, 0]];
restoreSel ← IF TiogaOps.NextPlaceholder[gotoend: FALSE].found THEN FALSE ELSE TRUE;
};
};
CreateChangeLog: Menus.ClickProc -- [parent: REF ANY, clientData: REF ANY ← NIL, mouseButton: Menus.MouseButton ← red, shift: BOOL ← FALSE, control: BOOL ← FALSE] -- = {
[] ← UpdateForSave[viewer: NARROW[parent], updateLastEdited: FALSE, updateChangeLog: viaChangeLogButton, insertPlaceHolders: TRUE, mergeWithPrevious: IF mouseButton = yellow THEN newSession -- i.e. new entry -- ELSE IF mouseButton = blue THEN continuation -- i.e. merge regardless -- ELSE ifSoonEnough];
};
Swap Icons
ForkViewerOp: PROC [proc: PROC [parent: Viewer, fileName: ROPE], parent: Viewer]
RETURNS [BOOL] = TRUSTED {
viewer: Viewer = ViewerTools.GetSelectedViewer[];
IF viewer=NIL OR (NOT viewer.iconic) OR (viewer.file=NIL)
THEN RETURN[FALSE];
TEditInput.CloseEvent[];
ForkCall[proc, parent, viewer.file];
RETURN [TRUE -- N.B.!! --] };
StackBottom: PROC
[proc: PROC [parent: Viewer, fileName: ROPE], parent: Viewer, fileName: ROPE] = {
proc[parent, fileName ! ABORTED => CONTINUE] };
ForkCall: PROC [proc: PROC [parent: Viewer, fileName: ROPE], parent: Viewer, fileName: ROPE] = TRUSTED {
Process.Detach[FORK StackBottom[proc, parent, fileName]];
-- fork so can release input monitor without waiting for call to complete
Process.Pause[Process.MsecToTicks[250]];
-- HACK: wait 1/4 sec to let other process lock the tdd before we return
};
LoadIconicOp: TiogaOps.CommandProc = {
RETURN[FALSE, ForkViewerOp[LoadIconicFile, viewer]];
};
LoadIconicFile: PUBLIC PROC [parent: ViewerClasses.Viewer, fileName: ROPE] = {
[] ← TiogaMenuOps.Load[parent, fileName];
};
OpenIconicOp: TiogaOps.CommandProc = {
RETURN [FALSE, ForkViewerOp[OpenIconicFile,viewer]];
};
OpenIconicFile: PUBLIC PROC [parent: ViewerClasses.Viewer, fileName: ROPE] = TRUSTED {
[] ← TEditDocuments2Impl.DoOpenFile[fileName, parent];
};
CloseAndOpenIconicOp: TiogaOps.CommandProc = {
RETURN [FALSE, ForkViewerOp[CloseAndOpenIconicFile,viewer]] };
CloseAndOpenIconicFile: PUBLIC PROC [parent: Viewer, fileName: ROPE] = {
[] ← TiogaMenuOps.CloseAndOpen[parent, fileName];
ViewerOps.PaintViewer[viewer: ViewerOps.FindViewer[parent.name], hint: all]; -- work around
};
Remembering last search
targetOfLastSearch: ROPE;
should all be one procedure if TiogaCommandProc were right type.
SaveLastSearch: PROC [viewer: Viewer, name: ATOM] RETURNS[quit: BOOLFALSE] = {
rope: ROPE = ViewerTools.GetSelectionContents[];
IF Rope.Length[rope] > 0 THEN targetOfLastSearch ← rope
ELSE IF NOT Rope.IsEmpty[targetOfLastSearch] THEN {
found: BOOL;
SELECT name FROM
$FindNext => found ← TiogaOps.FindText[viewer, targetOfLastSearch, forwards];
$FindAny => found ← TiogaOps.FindText[viewer, targetOfLastSearch, anywhere];
$FindPrev => found ← TiogaOps.FindText[viewer, targetOfLastSearch, backwards];
$FindNextWord => found ← TiogaOps.FindWord[viewer, targetOfLastSearch, forwards];
$FindAnyWord => found ← TiogaOps.FindWord[viewer, targetOfLastSearch, anywhere];
$FindPrevWord => found ← TiogaOps.FindWord[viewer, targetOfLastSearch, backwards];
$FindNextDef => found ← TiogaOps.FindDef[viewer, targetOfLastSearch, forwards];
$FindAnyDef => found ← TiogaOps.FindDef[viewer, targetOfLastSearch, anywhere];
$FindPrevDef => found ← TiogaOps.FindDef[viewer, targetOfLastSearch, backwards];
ENDCASE => ERROR;
quit ← TRUE;
IF NOT found THEN TRUSTED {
MessageWindow.Append[Rope.Concat[targetOfLastSearch, " not Found"], TRUE];
UserTerminal.BlinkDisplay[];
};
};
};
FindNext: TiogaOps.CommandProc = {
recordAtom ← FALSE;
quit ← SaveLastSearch[viewer, $FindNext];
};
FindAny: TiogaOps.CommandProc = {
recordAtom ← FALSE;
quit ← SaveLastSearch[viewer, $FindAny];
};
FindPrev: TiogaOps.CommandProc = {
recordAtom ← FALSE;
quit ← SaveLastSearch[viewer, $FindPrev];
};
FindNextWord: TiogaOps.CommandProc = {
recordAtom ← FALSE;
quit ← SaveLastSearch[viewer, $FindNextWord];
};
FindAnyWord: TiogaOps.CommandProc = {
recordAtom ← FALSE;
quit ← SaveLastSearch[viewer, $FindAnyWord];
};
FindPrevWord: TiogaOps.CommandProc = {
quit ← SaveLastSearch[viewer, $FindPrevWord];
};
FindNextDef: TiogaOps.CommandProc = {
recordAtom ← FALSE;
quit ← SaveLastSearch[viewer, $FindNextDef];
};
FindAnyDef: TiogaOps.CommandProc = {
recordAtom ← FALSE;
quit ← SaveLastSearch[viewer, $FindAnyDef];
};
FindPrevDef: TiogaOps.CommandProc = {
recordAtom ← FALSE;
quit ← SaveLastSearch[viewer, $FindPrevDef];
};
Initialization
updateLastEdited: BOOLEAN;
updateChangeLog: UpdateChange;
useLocks: BOOLEAN;
UpdateChange: TYPE = {automaticallyCreateNew, addIfPrevious, updateOnly, never, viaChangeLogButton};
MergeChange: TYPE = {continuation, newSession, ifSoonEnough};
mergeInterval: LONG CARDINAL ← LONG[3600] * 24; -- if two edits within this time, then consider edit as continuation of previous session, expressed in number of seconds
SetDefaultSwitches: UserProfile.ProfileChangedProc = {
r: ROPE;
updateLastEdited ← UserProfile.Boolean["UpdateLastEdited", TRUE];
r ← UserProfile.Token["UpdateChangeLog", "AddIfPrevious"];
updateChangeLog ← (SELECT TRUE FROM
Rope.Equal[r, "CreateNew", FALSE] => automaticallyCreateNew,
Rope.Equal[r, "AddIfPrevious", FALSE] => addIfPrevious,
Rope.Equal[r, "UpdateOnly", FALSE] => updateOnly,
ENDCASE => addIfPrevious);
IF updateChangeLog # never THEN {
IF Menus.FindEntry[menu: TiogaMenuOps.tiogaMenu, entryName: "ChangeLog"] = NIL THEN
Menus.AppendMenuEntry[
menu: TiogaMenuOps.tiogaMenu,
entry: Menus.CreateEntry[name: "ChangeLog", proc: CreateChangeLog, documentation: "Create/update change log entry"],
line: 0];
};
useLocks ← UserProfile.Boolean["NewStuff.UseLocks", TRUE];
};
UserProfile.CallWhenProfileChanges[SetDefaultSwitches];
TiogaOps.RegisterAbbrevFailedProc[Expand];
TiogaOps.RegisterCommand[name: $CtrlNextPlaceholder, proc: CtrlNextPlaceholder];
TiogaOps.RegisterCommand[name: $RedSave, proc: RedSave];
TiogaOps.RegisterCommand[$Load, LoadIconicOp];
TiogaOps.RegisterCommand[$Open, OpenIconicOp];
TiogaOps.RegisterCommand[$CloseAndOpen, CloseAndOpenIconicOp];
TiogaOps.RegisterCommand[$FindNext, FindNext];
TiogaOps.RegisterCommand[$FindAny, FindAny];
TiogaOps.RegisterCommand[$FindPrev, FindPrev];
TiogaOps.RegisterCommand[$FindNextWord, FindNextWord];
TiogaOps.RegisterCommand[$FindAnyWord, FindAnyWord];
TiogaOps.RegisterCommand[$FindPrevWord, FindPrevWord];
TiogaOps.RegisterCommand[$FindNextDef, FindNextDef];
TiogaOps.RegisterCommand[$FindAnyDef, FindAnyDef];
TiogaOps.RegisterCommand[$FindPrevDef, FindPrevDef];
-- TiogaOps.RegisterCommand[name: $NextPlaceholder, proc: NextPlaceholder];
END.
Edited on December 1, 1982 11:29 pm, by Teitelman
fixed bug in addIfPrevious, createNew so that if there were no non-comment nodes, would fail
fixed addIfPrevious to search through all children for "changes to"
use TiogaExtraOps.CompareLocOrder rather than SelectBranches
fix middle button continuation to add a new comments field.
changes to: UpdateChangeLog
Edited on December 2, 1982 1:29 pm, by Teitelman
fixed bug causing two appearances of ChangeLog.
fixed control-E not to stop scanning on *. Add PROGRAM name in case no .
changes to: UpdateLastEdited, ChangeLogEntry, UpdateChangeLog, SetDefaultSwitches, Expand0
Edited on December 2, 1982 3:09 pm, by Teitelman
changes to: Expand0
Edited on December 3, 1982 12:57 pm, by Teitelman
fixed bug relating to naked CR in non-node structure file causing loop in UpdateLastEdit
changes to: UpdateLastEdited
Edited on December 14, 1982 1:53 pm, by Teitelmann, SelectDate
Edited on December 14, 1982 2:05 pm, by Teitelman
fixed updatedate to keep looking after it finds an appropriate comment, and to change the last one it finds.
changes to: UpdateLastEdited, DIRECTORY
Edited on December 20, 1982 5:03 pm, by Teitelman
fixed FindLastChangeLog to stop on NIL node, rather than rootNode, because if the last node happens to have children, cauused infinite loop.
changes to: UpdateChangeLog
Edited on December 22, 1982 12:49 pm, by Teitelman
changes to: Expand0, UpdateLastEdited, TokenProc (local of IsADecl, local of CreateChangeEntry, local of UpdateChangeLog), IsADecl (local of CreateChangeEntry, local of UpdateChangeLog), CreateChangeEntry (local of UpdateChangeLog), UpdateChangeLog, IMPORTS, Expand
Edited on January 25, 1983 11:10 pm, by Teitelman
changes to: UpdateForSave
Edited on January 28, 1983 6:51 pm, by Teitelman
changes to: UpdateChangeLog, RedSave
Edited on February 1, 1983 9:32 pm, by Teitelman
changes to: RedSave, CloseAndOpenIconicFile, SetDefaultSwitches, IMPORTS, PrintStars (local of RedSave), Stars (local of RedSave)
Edited on February 9, 1983 5:17 pm, by Teitelman
changes to: RedSave, Stars (local of RedSave), sessionLog, Stars (local of RedSave)
Edited on March 10, 1983 3:02 am, by Teitelman
changes to: IMPORTS, RedSave
Edited on March 31, 1983 5:03 pm, by Teitelman
changes to: SaveLastSearch, Expand
Edited on May 9, 1983 6:13 pm, by Teitelman
changes to: IMPORTS, RedSave, proc (local of RedSave), proc (local of RedSave)
Edited on May 11, 1983 2:51 pm, by Teitelman
added catch phrase for SyntaxError to IsADecl. User had clicked changelog on a file that did not contain cedar and GetCedarToken blew up.
changes to: IsADecl (local of CreateChangeEntry, local of UpdateChangeLog)