EditorComfortsAImpl.mesa
Copyright © 1985, 1986 by Xerox Corporation. All rights reserved.
original author: Teitelman, May 11, 1983 2:53 pm
Maxwell, January 25, 1984 7:59:03 am PST
Russ Atkinson (RRA) August 25, 1986 12:59:54 pm PDT
Tim Diebert: January 13, 1986 11:39:27 am PST
Doug Wyatt, December 10, 1986 11:16:01 am PST
DIRECTORY
Ascii USING [Upper],
BasicTime USING [earliestGMT, GMT, Now, Period, Unpack],
Convert USING [Error, RopeFromInt, TimeFromRope],
FS USING [Error, StreamOpen],
IO USING [BreakProc, Close, EndOf, EndOfStream, Error, GetCedarTokenRope, GetChar, GetTime, GetTokenRope, PutF, PutFR, RIS, SkipWhitespace, STREAM, TokenProc],
Menus USING [AppendMenuEntry, ClickProc, CreateEntry, FindEntry, MenuEntry, MenuProc, ReplaceMenuEntry],
MessageWindow USING [Append],
Rope USING [Cat, Concat, Equal, Fetch, Find, FromChar, Index, IsEmpty, Length, Match, Replace, ROPE, Run, SkipOver, SkipTo, Substr],
TiogaOps USING [AddLooks, BackSpace, BackWord, Break, CallWithLocks, CaretAfter, CaretBefore, CaretOnly, ClearLooks, CommandProc, CompareNodeOrder, Delete, DeleteNextCharacter, FetchLooks, FirstChild, GetCaret, GetProp, GetRope, GetSelection, GetTextKey, GoToNextCharacter, GoToPreviousNode, InsertRope, InsertTime, IsComment, IsDirty, LastChild, LastLocWithin, Location, Nest, Next, NextPlaceholder, Parent, Previous, PutProp, PutTextKey, Ref, RegisterCommand, RestoreSelA, RestoreSelB, Root, SaveSelA, SaveSelB, SelectMatchingBrackets, SelectNodes, SelectPoint, SetComment, SetFormat, SetLooks, SetSelection, SetSelectionLooks, StepBackward, StepForward, SubtractLooks, UnNest, ViewerDoc],
TiogaOpsDefs USING [Ref],
TiogaMenuOps USING [tiogaMenu],
UserCredentials USING [Get],
UserProfile USING [Boolean, CallWhenProfileChanges, Line, ProfileChangedProc, Token],
ViewerClasses USING [Viewer],
ViewerTools USING [GetSelectionContents];
EditorComfortsAImpl: CEDAR MONITOR
IMPORTS Ascii, BasicTime, Convert, FS, IO, Menus, MessageWindow, Rope, TiogaMenuOps, TiogaOps, UserCredentials, UserProfile, ViewerTools
= BEGIN
Types
Location: TYPE = TiogaOps.Location;
ROPE: TYPE = Rope.ROPE;
STREAM: TYPE = IO.STREAM;
TiogaRef: TYPE = TiogaOps.Ref;
Viewer: TYPE = ViewerClasses.Viewer;
Monitored Data & Entry procedures
LogChange: ENTRY PROC [name: ROPE, description: ROPE] = {
ENABLE UNWIND => NULL;
outName: ROPE ← UserProfile.Token["EditorComforts.ChangesLog", NIL];
IF outName # NIL THEN {
We need to open up the file named and append to it.
out: STREAMNIL;
out ← FS.StreamOpen[fileName: outName, accessOptions: append
! FS.Error => IF error.group # bug THEN CONTINUE];
IF out = NIL THEN
out ← FS.StreamOpen[fileName: outName, accessOptions: create, keep: 4
! FS.Error => IF error.group # bug THEN GO TO nope];
There is a log file, so we can write to it.
IF description = NIL
THEN IO.PutF[out, "\nChanged: %g, %t\n", [rope[name]], [time[BasicTime.Now[]]]]
ELSE IO.PutF[out, "\nChanged: %g\n\t%g", [rope[name]], [rope[description]]];
IO.Close[out];
EXITS nope => {};
};
};
Control-Next
CtrlNextPlaceholder: TiogaOps.CommandProc = {
[viewer: Viewer ← NIL] RETURNS [recordAtom: BOOLTRUE, quit: BOOLFALSE]
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 {
+ 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
TiogaOps.SelectPoint[viewer: viewer, caret: [start.node, TiogaOps.GetTextKey[start.node, key].where + 2]];
};
IF TiogaOps.SelectMatchingBrackets['[, ']]
THEN [, start, end] ← TiogaOps.GetSelection[]
ELSE {
r: ROPE = TiogaOps.GetRope[start.node];
len: INT ← Rope.Length[r];
IF len > 1 AND start.node = end.node AND start.where < len AND Rope.Fetch[r, start.where] = '\001 AND end.where < len 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 loc.where IN [2..Rope.Length[r]+2) AND Rope.Fetch[r, loc.where - 2] = ': THEN
deletes the name:
TiogaOps.BackWord[];
loc ← TiogaOps.GetCaret[];
r ← TiogaOps.GetRope[loc.node];
SELECT TRUE FROM
loc.where NOT IN [1..Rope.Length[r]) => {};
Rope.Fetch[r, loc.where - 1] # '[ =>
not first argument, delete preceding space and comma
TiogaOps.BackSpace[2];
Rope.Fetch[r, loc.where] = ', =>
was first argument, but not last argument, delete next space and comma
TiogaOps.DeleteNextCharacter[2];
ENDCASE;
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[];
};
};
DoInner[Cleanup1, viewer];
};
Save operations: updating date, changelog
GetNiceUser: PROC RETURNS [ROPE] = {
user: ROPE ← UserCredentials.Get[].name;
i: INT ← Rope.Find[user, "."];
IF i # -1 THEN user ← Rope.Substr[base: user, len: i];
IF Rope.IsEmpty[user] THEN RETURN [user];
user ← Rope.Replace[user, 0, 1, Rope.FromChar[Ascii.Upper[Rope.Fetch[user, 0]]]];
RETURN [user];
};
GenLastEdited: PROC RETURNS [ROPE] = {
r: ROPE ← UserProfile.Line["EditorComforts.LastEdited", NIL];
IF r = NIL THEN r ← Rope.Concat[GetNiceUser[], ","];
RETURN [IO.PutFR["%g %g", [rope[r]], [time[BasicTime.Now[]]]]];
};
DoInner: PROC [inner: PROC [root: TiogaRef], viewer: Viewer] = {
root: TiogaRef = IF viewer # NIL THEN TiogaOps.ViewerDoc[viewer] ELSE NIL;
IF debugging
THEN inner[root]
ELSE TiogaOps.CallWithLocks[inner, root];
};
RedSave: TiogaOps.CommandProc = {
changeLogNode: TiogaRef;
description: ROPE;
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;
};
LogChange[viewer.name, description];
};
UpdateForSave: PROC [viewer: Viewer, updateLastEdited: BOOL, updateChangeLog: UpdateChange, insertPlaceHolders: BOOL, mergeWithPrevious: MergeChange] RETURNS[changeLogNode: TiogaRef] = {
inner: PROC [root: TiogaRef] = {
user: ROPE ← GetNiceUser[];
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.
user ← UserProfile.Line["EditorComforts.LastEdited", user];
IF updateLastEdited THEN UpdateLastEdited[viewer, root, user];
IF updateChangeLog # never THEN [changeLogNode, restoreSel] ← UpdateChangeLog[viewer, root, user, updateChangeLog, insertPlaceHolders, mergeWithPrevious];
IF restoreSel THEN TiogaOps.RestoreSelA[];
};
DoInner[inner, viewer];
};
monthList: LIST OF ROPE = LIST [
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
FindDate: PROC [r: ROPE] RETURNS [i, j: INT] = {
... returns first and last index in where a valid date occurs; [-1, -1] indicates no valid date.
len: INT = Rope.Length[r];
IF len > maxDateRope THEN RETURN [-1, -1];
FOR months: LIST OF ROPE ← monthList, months.rest UNTIL months = NIL DO
start: INT ← 0;
WHILE start < len DO
i ← Rope.Index[r, start, months.first, TRUE];
IF i = len THEN EXIT; -- not this month
SELECT TRUE FROM
(j ← Rope.Find[r, "ST", i]) # -1 => {j ← j + 1};
(j ← Rope.Find[r, "DT", i]) # -1 => {j ← j + 1};
(j ← Rope.Find[r, " am", i, FALSE]) # -1 => {j ← j + 2};
(j ← Rope.Find[r, " pm", i, FALSE]) # -1 => {j ← j + 2};
ENDCASE => EXIT; -- no plausible ending!
[] ← Convert.TimeFromRope[Rope.Substr[r, i, j - i + 1]
! Convert.Error => {
month may conflict with someone's name, so skip this and try further on
start ← start + 3; LOOP}];
RETURN[i, j];
ENDLOOP;
ENDLOOP;
RETURN[-1, -1];
};
SelectDate: PROC [viewer: Viewer, loc: Location, rope: ROPE] RETURNS [BOOL] = {
loc is the location corresponding to rope. In most cases, where is 0 and r is GetRope. If rope contains a date, this procedure selects the date and returns 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] = {
s: STREAMNIL;
next: INT ← 0; -- beginning of next line
node: TiogaRef ← NIL;
nodeRope: ROPENIL;
current: INT ← 0;
r: ROPENIL;
foundOne: BOOLFALSE;
insertAt: Location ← [NIL, 0]; -- where to insert the last-edited comment.
newInsertAtNode: BOOLTRUE; -- please assign the next node to insertAt
foundLastEditedNode: BOOLFALSE; -- we found a last-edited comment
TiogaOps.SaveSelA[];
DO
Enumerate the nodes and the lines with each node. Look for the user's name followed by the date. (There may be more than one; take the last one.) If there is no such thing, then we want to insert a new last-edited comment after the final last-edited comment, or after all of the comments.
SELECT TRUE FROM
node = NIL => {
first time
node ← TiogaOps.FirstChild[rootNode];
nodeRope ← TiogaOps.GetRope[node];
TiogaOps.SelectPoint[viewer: viewer, caret: [node: node, where: 0]];
next ← -1;
};
next = -1 => {
just exhausted a node
node ← TiogaOps.StepForward[node];
IF node = NIL THEN EXIT;
IF newInsertAtNode AND insertAt.node # node AND NOT Rope.IsEmpty[nodeRope]
THEN insertAt ← [node, 0];
IF foundLastEditedNode THEN newInsertAtNode ← FALSE;
nodeRope ← TiogaOps.GetRope[node];
current ← 0;
}
ENDCASE => {
try the next line
current ← next + 1;
IF newInsertAtNode AND NOT Rope.IsEmpty[r] THEN insertAt ← [node, current];
IF foundLastEditedNode THEN newInsertAtNode ← FALSE;
};
IF TiogaOps.GetProp[n: node, name: $Mark] # NIL THEN GO TO done;
IF TiogaOps.IsComment[node]
THEN r ← nodeRope
ELSE {
check to see if it is a mesa comment (begins with two -'s or /'s)
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;
IF ~IsMesaComment[r, s] THEN EXIT;
};
IF Rope.IsEmpty[r] THEN EXIT;
IF Rope.Find[s1: r, s2: user, case: FALSE] # -1 AND SelectDate[viewer: viewer, loc: [node, current], rope: r] THEN {
modify the last one, so user can have separate created and edited comments.
TiogaOps.SaveSelB[];
foundOne ← TRUE;
};
IF ~foundOne AND Rope.Find[s1: r, s2: "Last Edited by:"] # -1
THEN newInsertAtNode ← foundLastEditedNode ← TRUE;
ENDLOOP;
IF foundOne THEN {
We just have a date to update (the most frequent case).
TiogaOps.RestoreSelB[];
TiogaOps.Delete[];
TiogaOps.InsertTime[];
GO TO done;
};
not found, insert new date string at top.
IF insertAt.node = NIL THEN RETURN; -- no comments seen
TiogaOps.SelectPoint[viewer: viewer, caret: insertAt];
IF insertAt.where = 0 THEN {
empty: BOOL ← Rope.IsEmpty[TiogaOps.GetRope[insertAt.node]];
TiogaOps.Break[];
IF empty THEN TiogaOps.GoToPreviousNode[];
};
TiogaOps.CaretOnly[];
TiogaOps.ClearLooks[];
IF TiogaOps.IsComment[TiogaOps.StepBackward[insertAt.node]]
THEN {
inserting before a node
ForceToLevel[1, TRUE];
TiogaOps.InsertRope[GenLastEdited[]];
}
ELSE {
inserting inside of a node, or no node structure
TiogaOps.InsertRope["-- "];
TiogaOps.InsertRope[GenLastEdited[]];
IF insertAt.where # 0 THEN TiogaOps.InsertRope["\n"];
};
GO TO done;
EXITS done => TiogaOps.RestoreSelA[];
};
IsMesaComment: PROC[rope: ROPE, s: STREAM] RETURNS[BOOL] = {
ENABLE IO.EndOfStream => GO TO nope;
c: CHAR;
s ← IO.RIS[rope, s];
[] ← s.SkipWhitespace[flushComments: FALSE];
IF s.EndOf[] THEN RETURN[TRUE]; -- line with just white space.
c ← s.GetChar[];
RETURN[(c = '- OR c = '/) AND NOT s.EndOf[] AND (c = s.GetChar[])];
EXITS nope => RETURN [FALSE];
};
ChangeEntry: TYPE = RECORD[
rope: ROPE,
path: ROPENIL,
node: TiogaRef,
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: 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: PROC [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 => GO TO nope;
IO.Error => IF ec = SyntaxError THEN GO TO nope;
};
name ← IO.GetCedarTokenRope[s].token;
IF Rope.Equal[IO.GetCedarTokenRope[s].token, ":"] AND (Rope.Find[r, "{"] # -1 OR Rope.Find[r, "BEGIN"] # -1) THEN RETURN[name];
EXITS nope => {};
};
};
AlreadySeen: PROC [node: TiogaRef] RETURNS [BOOL] = {
FOR l: LIST OF REF ChangeEntry ← changed, l.rest UNTIL l = NIL DO
SELECT l.first.node FROM
NIL => entriesToBeMerged ← TRUE;
node => RETURN[TRUE];
ENDCASE;
ENDLOOP;
RETURN[FALSE];
};
depth: INT ← 0;
node1 ← node;
UNTIL node1 = rootNode DO
name: ROPE;
SELECT TRUE FROM
TiogaOps.IsComment[node1] => {};
change = NIL AND AlreadySeen[node1] => RETURN;
(name ← IsADecl[node1]) = NIL => node0 ← node1;
change = NIL => change ← NEW[ChangeEntry ← [node: node1, rope: name]];
ENDCASE => {
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.GetCedarTokenRope[s !
IO.Error, IO.EndOfStream => CONTINUE].token]];
};
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
First, get the current change log entry from the Tioga property. Ignore it unless it is not NIL and it is the right type.
WITH TiogaOps.GetProp[n: rootNode, name: $ChangeLog] SELECT FROM
entry: REF ChangeLogEntry => currentChangeLogEntry ← entry;
ENDCASE;
SELECT TRUE FROM
mergeWithPrevious = newSession AND currentChangeLogEntry # NIL => {
start new session even if currently working on a session.
currentChangeLogEntry ← NIL;
changed ← tailOfChanged ← NIL;
};
currentChangeLogEntry # NIL => {
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;
tailOfChanged ← changed ← currentChangeLogEntry.changed;
IF tailOfChanged # NIL THEN DO
rest: LIST OF REF ChangeEntry ← tailOfChanged.rest;
IF rest = NIL THEN EXIT;
tailOfChanged ← rest;
ENDLOOP;
EXITS
NoLog => currentChangeLogEntry ← NIL;
};
ENDCASE;
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: PROC RETURNS [TiogaRef ← NIL] = {
n: TiogaRef ← end.node;
r: ROPENIL;
find last non-empty comment node.
DO
IF n = NIL THEN RETURN;
r ← TiogaOps.GetRope[n];
SELECT TRUE FROM
Rope.IsEmpty[r] => {};
NOT TiogaOps.IsComment[n] => RETURN;
SelectDate[viewer: viewer, loc: [n, 0], rope: r] => EXIT;
ENDCASE => RETURN;
n ← TiogaOps.Previous[n, rootNode];
ENDLOOP;
};
IfSoonEnough: PROC RETURNS [BOOL] = {
nowInSeconds, thenInSeconds: LONG CARDINAL;
then: ROPE = ViewerTools.GetSelectionContents[];
time: BasicTime.GMTIO.GetTime[IO.RIS[then]
! IO.Error => GO TO bogus; IO.EndOfStream => GO TO bogus];
nowInSeconds ← BasicTime.Period[BasicTime.earliestGMT, BasicTime.Now[]];
thenInSeconds ← BasicTime.Period[BasicTime.earliestGMT, time];
RETURN[nowInSeconds - thenInSeconds < mergeInterval];
EXITS bogus => RETURN [FALSE];
};
SELECT updateChangeLog FROM
updateOnly => RETURN;
viaButton => NULL;
user clicked ChangeLog button. create one if not there
createNew => {
create a new one if looks like source file.
n: TiogaRef ← end.node;
WHILE n # NIL DO
IF NOT TiogaOps.IsComment[n] THEN {
r: ROPE = TiogaOps.GetRope[n];
SELECT TRUE FROM
Rope.IsEmpty[r] => {};
Rope.Match["*END.*", r], Rope.Match["*}.*", r] => GO TO foundCode;
ENDCASE => RETURN;
};
n ← TiogaOps.Previous[n, rootNode];
ENDLOOP;
RETURN;
EXITS foundCode => {};
};
addIfPrevious =>
add one if already one there of appropriate form.
IF (previousEntryNode ← FindLastChangeLog[]) = NIL THEN RETURN;
ENDCASE => RETURN;
SELECT TRUE FROM
mergeWithPrevious = newSession => previousEntryNode ← NIL;
updateChangeLog # addIfPrevious => previousEntryNode ← FindLastChangeLog[];
ENDCASE;
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 {
r: ROPE ← TiogaOps.GetRope[previousEntryNode];
change: REF ChangeEntry;
s ← IO.RIS[Rope.Substr[r, Rope.Find[r, ":"] + 1]];
WHILE ~s.EndOf[] DO
r ← IO.GetTokenRope[s ! IO.EndOfStream => EXIT].token;
change ← NEW[ChangeEntry ← [rope: r, node: NIL, written: TRUE]];
IF changed = NIL
THEN {
changed ← LIST[change];
tailOfChanged ← changed;
}
ELSE {
tailOfChanged.rest ← LIST[change];
tailOfChanged ← tailOfChanged.rest;
};
r ← IO.GetTokenRope[s ! IO.EndOfStream => EXIT].token;
IF Rope.Equal[r, "("] THEN {
read "Local to"
rparProc: IO.BreakProc = {
RETURN[IF char = ') THEN sepr ELSE other];
};
change.path ← IO.GetTokenRope[s, rparProc ! IO.EndOfStream => EXIT].token;
r ← IO.GetTokenRope[s ! IO.EndOfStream => EXIT].token;
};
WHILE r # NIL DO -- skip over any comments that the user may have inserted
IF Rope.Equal[r, ","] THEN EXIT;
r ← IO.GetTokenRope[s ! IO.EndOfStream => EXIT].token;
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;
SELECT TRUE FROM
NOT writeAll AND l.first.written => {};
TiogaOps.CompareNodeOrder[node1: rootNode, node2: l.first.node] = disjoint => {
IF lag = NIL THEN changed ← changed.rest ELSE lag.rest ← l.rest;
};
TiogaOps.IsComment[l.first.node] => RETURN;
ENDCASE => {anyChanges ← TRUE; EXIT};
lag ← l;
ENDLOOP;
tell the user what we decided to do
IF updateChangeLog = viaButton
THEN {
SELECT TRUE FROM
NOT anyChanges AND mergeWithPrevious = ifSoonEnough =>
MessageWindow.Append["No (new) changes found.", TRUE];
currentChangeLogEntry = NIL => {
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.
};
previousEntryNode # NIL =>
MessageWindow.Append["Continuing with previous edit session.", TRUE];
ENDCASE;
}
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[];
ForceToLevel[0, TRUE];
TiogaOps.InsertRope[GenLastEdited[]];
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[];
SELECT TRUE FROM
NOT insertPlaceHolders => DeletePlaceHolders[];
NOT first => {
something inserted with placeHolders
TiogaOps.SelectPoint[viewer: viewer, caret: [currentChangeLogEntry.node, 0]];
restoreSel ← IF TiogaOps.NextPlaceholder[gotoend: FALSE].found THEN FALSE ELSE TRUE;
};
ENDCASE;
};
CreateChangeLog: Menus.MenuProc = {
[parent: REF, clientData: REF, mouseButton: MouseButton, shift: BOOL, control: BOOL]
[] ← UpdateForSave[viewer: NARROW[parent], updateLastEdited: FALSE, updateChangeLog: viaButton, insertPlaceHolders: TRUE, mergeWithPrevious: IF mouseButton = yellow THEN newSession -- i.e. new entry -- ELSE IF mouseButton = blue THEN continuation -- i.e. merge regardless -- ELSE ifSoonEnough];
};
AddCopyrightProc: Menus.MenuProc = {
[parent: REF, clientData: REF, mouseButton: MouseButton, shift: BOOL, control: BOOL]
viewer: ViewerClasses.Viewer = NARROW[parent];
empty: BOOLTRUE;
inner: PROC [root: TiogaRef] = {
nodesSearched: INT ← 0;
insertPlain: BOOLTRUE;
contents: ROPENIL;
year: ROPE ← Convert.RopeFromInt[BasicTime.Unpack[BasicTime.Now[]].year];
FOR node: TiogaRef ← root, TiogaOps.StepForward[node] WHILE node # NIL DO
copyPos: INT
← Rope.Find[contents ← Rope.Substr[TiogaOps.GetRope[node], 0, 1024], "Copyright "];
IF TiogaOps.IsComment[node] THEN insertPlain ← FALSE;
SELECT TRUE FROM
copyPos >= 0 => {
There is a "Copyright" message in this node, so we should try to update the year. We try to insert the year after the last year mentioned if the current year is not already present.
pos: INT ← copyPos+9;
IF Rope.Run[contents, pos, " c "]=3 THEN {
Replace obsolete math font symbol with proper Xerox character code.
TiogaOps.SetSelection[viewer: viewer, start: [node, pos], end: [node, pos+2], pendingDelete: TRUE];
TiogaOps.SetSelectionLooks[];
TiogaOps.InsertRope[" Ó "];
};
contents ← Rope.Substr[contents, 0, Rope.SkipTo[contents, pos, "\n"]];
don't look farther than the next end-of-line for the year
IF Rope.Find[contents, year, pos] < 0 THEN {
The current year is not here.
lastNum: INT ← 0;
FOR i: INT IN [pos..Rope.Length[contents]) DO
SELECT Rope.Fetch[contents, i] FROM
'\n => EXIT;
IN ['0..'9] => lastNum ← i+1;
ENDCASE;
ENDLOOP;
IF lastNum > 0 THEN {
Insert the current year just after the last number. This is not the greatest algorithm, but it will have to do until someone brighter comes along.
TiogaOps.SetSelection[viewer, [node, lastNum], [node, lastNum]];
TiogaOps.ClearLooks[caret];
TiogaOps.InsertRope[", "];
TiogaOps.InsertRope[year];
};
};
RETURN;
};
(nodesSearched ← nodesSearched + 1) > maxNodesToSearch => {
insertPlain ← FALSE;
EXIT;
};
ENDCASE;
ENDLOOP;
At this point we need to add a new copyright notice. We do it in plain text if the document does not look like it has significant node structure, otherwise we do it using tioga formatting.
FOR node: TiogaRef ← root, TiogaOps.StepForward[node] WHILE node # NIL DO
len: INT ← Rope.Length[contents ← TiogaOps.GetRope[node]];
IF TiogaOps.IsComment[node] THEN insertPlain ← FALSE;
IF len < 32 AND Rope.SkipOver[contents, 0, " \n\t"] = len THEN {
no substantive contents in this node, so skip it
LOOP};
empty ← FALSE;
TiogaOps.SelectNodes[viewer, node, node];
EXIT;
ENDLOOP;
IF empty THEN TiogaOps.SelectNodes[viewer, root, root];
IF insertPlain
THEN {
Insert the plain text message
pos: INT ← Rope.SkipTo[contents, 1, "\n"] + 1;
notice: ROPE ← Rope.Cat["-- Copyright (C) ", year, " by Xerox Corporation. All rights reserved.\n"];
TiogaOps.GoToNextCharacter[pos];
TiogaOps.ClearLooks[caret];
TiogaOps.InsertRope[notice];
}
ELSE {
Insert the formatted text message
notice: ROPE ← Rope.Cat["Copyright Ó ", year, " by Xerox Corporation. All rights reserved."];
TiogaOps.CaretAfter[];
TiogaOps.Break[];
ForceToLevel[1, TRUE];
TiogaOps.InsertRope[notice];
};
};
DoInner[inner, viewer];
};
ForceToLevel: PROC [n: NAT ← 0, setup: BOOL] = {
level: NAT ← GetLevel[TiogaOps.GetSelection[].start.node];
SELECT level FROM
< n => THROUGH [level..n) DO TiogaOps.Nest[]; ENDLOOP;
> n => THROUGH [n..level) DO TiogaOps.UnNest[]; ENDLOOP;
ENDCASE;
IF setup THEN {
TiogaOps.SetFormat[nodeFormat];
TiogaOps.ClearLooks[caret];
TiogaOps.SetComment[];
};
};
GetLevel: PROC [ref: TiogaRef] RETURNS [level: NAT ← 0] = {
IF ref # NIL THEN {
root: TiogaRef ← TiogaOps.Root[ref];
IF ref # root THEN DO
ref ← TiogaOps.Parent[ref];
IF ref = root THEN RETURN;
level ← level + 1;
ENDLOOP;
};
};
Initialization
debugging: BOOLFALSE;
updateLastEdited: BOOLTRUE;
updateChangeLog: UpdateChange ← never;
UpdateChange: TYPE = {createNew, addIfPrevious, updateOnly, never, viaButton};
MergeChange: TYPE = {continuation, newSession, ifSoonEnough};
mergeInterval: LONG CARDINALLONG[3600] * 24;
if two edits within this time, then consider edit as continuation of previous session, expressed in number of seconds
SetDefaultSwitches: UserProfile.ProfileChangedProc = {
ENABLE UNWIND => NULL;
r: ROPE ← UserProfile.Token["EditorComforts.UpdateChangeLog", "AddIfPrevious"];
updateLastEdited ← UserProfile.Boolean["EditorComforts.UpdateLastEdited", TRUE];
updateChangeLog ← (SELECT TRUE FROM
Rope.Equal[r, "CreateNew", FALSE] => createNew,
Rope.Equal[r, "AddIfPrevious", FALSE] => addIfPrevious,
Rope.Equal[r, "UpdateOnly", FALSE] => updateOnly,
ENDCASE => never);
};
InstallMenuButton: PROC [name: ROPE, proc: Menus.MenuProc] = {
old: Menus.MenuEntry =
Menus.FindEntry[menu: TiogaMenuOps.tiogaMenu, entryName: name];
new: Menus.MenuEntry =
Menus.CreateEntry[name: name, proc: proc];
IF old = NIL
THEN Menus.AppendMenuEntry[TiogaMenuOps.tiogaMenu, new]
ELSE Menus.ReplaceMenuEntry[TiogaMenuOps.tiogaMenu, old, new];
};
Init: PROC = {
InstallMenuButton["(C)", AddCopyrightProc];
InstallMenuButton["Log", CreateChangeLog];
UserProfile.CallWhenProfileChanges[SetDefaultSwitches];
TiogaOps.RegisterCommand[name: $CtrlNextPlaceholder, proc: CtrlNextPlaceholder];
TiogaOps.RegisterCommand[name: $RedSave, proc: RedSave];
};
plainTextMessage: ROPE ← "-- Copyright (C) 1985 by Xerox Corporation. All rights reserved.\n";
tiogaMessage: ROPE ← "Copyright © 1985 by Xerox Corporation. All rights reserved.";
maxNodesToSearch: INT ← 6;
minNodesForFormat: INT ← 3;
nodeFormat: ROPE ← "code";
maxDateRope: NAT ← 256;
In FindDate, ropes that are longer than this length are assumed to NOT have embedded dates. This avoids searches of ludicrous length.
Init[];
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 GetCedarTokenRope blew up.
changes to: IsADecl (local of CreateChangeEntry, local of UpdateChangeLog)
Edited on January 3, 1984 2:16 pm, by Maxwell
converted to Cedar 5. moved procedures that used the UserExec to end of file. commented out references to UserExecExtras in RedSave.
Edited on June 15, 1984 2:21:59 pm PDT, by Russ Atkinson
fixed FindDate to ensure that what it finds is really a date. This allows the appearance of last names like Mayo, August, and the like. Defensive programming wins again!
Russ Atkinson (RRA) January 3, 1985 5:51:17 pm PST
Various cleanups, especially the change to "EditorComforts". We have added the ability to insert Xerox copyright notices. We have also thrown out a number of "features". The format of various messages has been simplified & made more uniform.
Russ Atkinson (RRA) April 29, 1985 3:56:23 pm PDT
fixed FindDate to ignore ropes of excessive length (governed by maxDateRope)