EditorComfortsAImpl.mesa
Copyright © 1985 by Xerox Corporation. All rights reserved.
Teitelman, May 11, 1983 2:53 pm
Maxwell, January 25, 1984 7:59:03 am PST
Russ Atkinson (RRA) February 21, 1985 1:34:23 pm 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, 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, 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 [GetSelectedViewer, 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: STREAM ← NIL;
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: BOOL ← TRUE, quit: BOOL ← FALSE]
Cleanup1:
PROC [root: TiogaRef] = {
viewer: Viewer;
start, end: Location;
key: REF ANY;
fieldsRemain: BOOL ← FALSE;
[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: BOOL ← TRUE;
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 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];
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];
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 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] = {
s: STREAM ← NIL;
next: INT ← 0; -- beginning of next line
node: TiogaRef ← NIL;
nodeRope: ROPE ← NIL;
current: INT ← 0;
r: ROPE ← NIL;
foundOne: BOOL ← FALSE;
insertAt: Location ← [NIL, 0]; -- where to insert the last-edited comment.
newInsertAtNode: BOOL ← TRUE; -- please assign the next node to insertAt
foundLastEditedNode: BOOL ← FALSE; -- 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.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[];
TiogaOps.RestoreSelA[];
RETURN;
};
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[];
};
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"];
};
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: ROPE ← NIL,
node: TiogaRef,
written: BOOL ← FALSE];
ChangeLogEntry: TYPE = RECORD[
changed: LIST OF REF ChangeEntry,
node: TiogaRef];
UpdateChangeLog:
PROC [viewer: Viewer, rootNode: TiogaRef, user:
ROPE, updateChangeLog: UpdateChange, insertPlaceHolders:
BOOL ←
FALSE, mergeWithPrevious: MergeChange]
RETURNS [changeLogNode: TiogaRef ←
NIL, restoreSel:
BOOL ←
TRUE] = {
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: BOOL ← TRUE;
writeAll: BOOL ← TRUE;
anyChanges: BOOL ← FALSE;
CreateChangeEntry:
PROC [node: TiogaRef] = {
change: REF ChangeEntry;
node0, node1: TiogaRef;
entriesToBeMerged: BOOL ← FALSE;
IsADecl:
PROC [node: TiogaRef]
RETURNS[declName:
ROPE ←
NIL] = {
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: ROPE ← NIL;
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.
GMT ←
IO.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];
inner:
PROC [root: TiogaRef] = {
nodesSearched: INT ← 0;
insertPlain: BOOL ← TRUE;
contents: ROPE ← NIL;
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;
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};
TiogaOps.SelectNodes[viewer, node, node];
EXIT;
ENDLOOP;
IF insertPlain
THEN {
Insert the plain text message
pos: INT ← Rope.SkipTo[contents, 1, "\n"] + 1;
TiogaOps.GoToNextCharacter[pos];
TiogaOps.ClearLooks[caret];
TiogaOps.InsertRope[plainTextMessage];
}
ELSE {
Insert the formatted text message
pos: INT ← Rope.Find[tiogaMessage, " c "] + 1; -- position of the c
TiogaOps.CaretAfter[];
TiogaOps.Break[];
ForceToLevel[1, TRUE];
TiogaOps.InsertRope[tiogaMessage];
IF pos > 0
THEN {
There is a copyright symbol to be made in to look "m" (Math)
here: TiogaRef ← TiogaOps.GetSelection[].start.node;
TiogaOps.SetSelection[viewer, [here, pos], [here, pos]];
TiogaOps.SetLooks["m", selection];
};
};
};
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: BOOL ← FALSE;
updateLastEdited: BOOL ← TRUE;
updateChangeLog: UpdateChange ← never;
UpdateChange: TYPE = {createNew, addIfPrevious, updateOnly, never, viaButton};
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 = {
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";
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.