TiogaComfortsImpl.mesa
Copyright Ó 1990, 1991, 1992, 1993 by Xerox Corporation. All rights reserved.
Last changed by Pavel on February 12, 1990 4:13 pm PST
Willie-s, June 27, 1991 10:44 am PDT
Michael Plass, September 24, 1991 2:22 pm PDT
Doug Wyatt, February 28, 1992 10:20 am PST
Last tweaked by Mike Spreitzer September 2, 1993 2:56 pm PDT
DIRECTORY
Ascii USING [Upper],
BasicTime USING [Now, Unpack],
Convert USING [Error, RopeFromTime, TimeFromRope],
EditSpan USING [ChangeNesting, InsertTextNode, Place],
IO USING [PutFR, PutFR1],
Menus USING [AppendMenuEntry, ClickProc, CreateEntry, FindEntry, MenuEntry, ReplaceMenuEntry],
Rope USING [Concat, Fetch, Find, Flatten, FromChar, Index, IsEmpty, IsPrefix, MaxLen, Replace, ROPE, Run, Size, SkipOver, SkipTo, Substr, Text],
SystemNames USING [UserName],
TEditInput USING [CommandProc, CurrentEvent, InterpretAtom, Register],
TEditLocks USING [Lock, Unlock],
TextEdit USING [ChangeLooks, FetchLooks, GetComment, GetFormat, Looks, noLooks, PutComment, PutFormat, ReplaceByRope],
TextEditBogus USING [GetRope],
TextNode USING [Level, Location, NodeItself, Ref, Span, StepForward],
TiogaMenuOps USING [tiogaMenu],
TiogaOps USING [Ref, ViewerDoc],
UserProfile USING [Line];
TiogaComfortsImpl: CEDAR PROGRAM
IMPORTS Ascii, BasicTime, Convert, EditSpan, IO, Menus, Rope, SystemNames, TEditInput, TEditLocks, TextEdit, TextEditBogus, TextNode, TiogaMenuOps, TiogaOps, UserProfile
~ BEGIN
ROPE: TYPE ~ Rope.ROPE;
Node: TYPE ~ TextNode.Ref;
IsComment: PROC [node: Node] RETURNS [BOOL] ~ {
RETURN [TextEdit.GetComment[node]]
};
PrefixAns: TYPE
~ RECORD [prefixLength: INT, lineTerminable: BOOL, opener, closer: ROPE];
CommentPrefix: PROC [rope: ROPE, start: INT] RETURNS [PrefixAns] ~ {
IF (rope.Run[start, "--"] = 2) THEN RETURN [[2, TRUE, NIL, "--"]]
ELSE IF (rope.Run[start, "//"] = 2) THEN RETURN [[2, TRUE, NIL, NIL]]
ELSE IF rope.Run[start, ";"] = 1 THEN
RETURN [[rope.SkipOver[start, ";"] - start, TRUE, NIL, NIL]]
ELSE IF rope.Run[start, "(*"] = 2 THEN RETURN [[2, FALSE, "(*", "*)"]]
ELSE IF rope.Run[start, "/*"] = 2 THEN RETURN [[2, FALSE, "/*", "*/"]]
ELSE RETURN [[0, TRUE, NIL, NIL]];
};
SkipOverBackward: PROC [s: ROPE, pos: INT ¬ Rope.MaxLen, skip: ROPE] RETURNS [INT] ~ {
Like Rope.SkipOver, but backwards. Returns the highest position N in s such that s[N] is NOT in the skip string and N <= pos. If no such character occurs in s, then return -1.
skipText: Rope.Text = skip.Flatten[];
skiplen: NAT ~ IF skipText = NIL THEN 0 ELSE skipText.Size[];
slen: INT ~ s.Size[];
start: INT ~ MIN[slen-1, pos];
IF start < 0 OR s.IsEmpty[] THEN RETURN [-1];
IF skiplen=0 THEN RETURN[start];
FOR n: INT ¬ start, n - 1 WHILE n>=0 DO
c: CHAR ~ s.Fetch[n];
FOR i: NAT IN [0..skiplen) DO
IF c = skipText[i] THEN EXIT;
REPEAT
FINISHED => RETURN [n];
ENDLOOP;
ENDLOOP;
RETURN [-1];
};
EnumerateInitialCommentLines: PROC [
root: Node,
proc: PROC [text: ROPE, node: Node, start, length, after: INT, hasLineBreak: BOOL] RETURNS [continue: BOOL]]
RETURNS [finalNode: Node ¬ NIL, finalStart, finalAfter: INT ¬ 0] ~ {
Calls proc on the text and location of each line of comments at the beginning of the given document until proc returns FALSE. Returns the location of the last comment line found. In all cases, the ``line'' is not considered to include any opening or closing comment characters (e.g., "--" or ";"), leading or trailing whitespace, or the terminating newline character if any.
index: INT ¬ 0;
node: Node ¬ TextNode.StepForward[root];
contents: ROPE ¬ TextEditBogus.GetRope[node];
hasLineBreak: BOOL ¬ TRUE;
DO
GetTrimmedLine: PROC [begin: INT] RETURNS [start, length: INT, bad: BOOL ¬ FALSE] ~ {
lim: INT ¬ contents.Size[];
lineBreak: INT ¬ contents.SkipTo[begin, "\l\r"];
fin: INT; --index of first char beyond comment contents
IF pa.closer=NIL THEN {
IF NOT pa.lineTerminable THEN ERROR;
fin ¬ lineBreak;
index ¬ fin + 1; -- set up for next line
hasLineBreak ¬ lineBreak<lim}
ELSE {
after: INT; --index of terminating linebreak or first char after closer
IF pa.lineTerminable THEN lim ¬ lineBreak;
IF pa.opener#NIL THEN {
depth: INT ¬ 1;
lo: INT ¬ contents.Index[begin, pa.opener];
lc: INT ¬ contents.Index[begin, pa.closer];
fin ¬ begin;
WHILE depth > 0 AND lc < lim DO
IF lo<lc THEN {depth ¬ depth + 1;
after ¬ lo+pa.opener.Size[];
IF lc<after THEN lc ¬ contents.Index[after, pa.closer];
lo ¬ contents.Index[after, pa.opener]}
ELSE IF lc<lo THEN {depth ¬ depth - 1;
after ¬ (fin ¬ lc) + pa.closer.Size[];
IF depth>0 THEN {
IF lo<after THEN lo ¬ contents.Index[after, pa.opener];
lc ¬ contents.Index[after, pa.closer]}}
ELSE EXIT;
ENDLOOP;
IF depth>0 THEN after ¬ fin ¬ lim}
ELSE {
fin ¬ contents.Index[begin, pa.closer];
IF fin<lim THEN after ¬ fin + pa.closer.Size[] ELSE after ¬ fin ¬ lim;
};
index ¬ contents.SkipTo[after, "\l\r"];
bad ¬ index>after AND SkipOverBackward[contents, index-1, " \t"]>=after;
Disregard comments followed by non-white chars.
IF (hasLineBreak ¬ index < contents.Size[]) THEN index ¬ index+1;
};
start ¬ contents.SkipOver[begin, " \t"];
fin ¬ SkipOverBackward[contents, fin - 1, " \t"];
length ¬ MAX[0, fin - start + 1];
RETURN};
start, length: INT;
pa: PrefixAns;
IF (pa ¬ CommentPrefix[contents, index]).prefixLength > 0 OR IsComment[node] THEN {
savedIndex: INT ~ index;
bad: BOOL;
[start, length, bad] ¬ GetTrimmedLine[index + pa.prefixLength];
IF bad THEN EXIT;
IF length > 0 OR pa.prefixLength > 0 THEN { -- don't count truly empty comments
finalNode ¬ node; -- remember this one in case it's the last one
finalStart ¬ savedIndex;
finalAfter ¬ index;
IF NOT proc[contents.Substr[start, length], node, start, length, index, hasLineBreak] THEN EXIT;
};
}
ELSE
EXIT;
IF index >= contents.Size[] THEN { -- on to the next node
node ¬ TextNode.StepForward[node];
index ¬ 0;
IF node = NIL THEN EXIT;
contents ¬ TextEditBogus.GetRope[node];
};
ENDLOOP;
};
CopyrightButton: Menus.ClickProc ~ {
TEditInput.InterpretAtom[parent, $UpdateCopyrightNotice];
};
UpdateCopyrightNotice: TEditInput.CommandProc ~ {
[viewer: ViewerClasses.Viewer] RETURNS [recordAtom: BOOL ¬ TRUE, quit: BOOL ¬ FALSE]
root: Node ~ TiogaOps.ViewerDoc[viewer];
hasNodes: BOOL ~ ( TextNode.StepForward[TextNode.StepForward[root]] # NIL );
Inner: PROC ~ {
year: INT ~ BasicTime.Unpack[BasicTime.Now[]].year;
done: BOOL ¬ FALSE;
afterFirst: INT ¬ 0;
firstHasLinebreak: BOOL ¬ FALSE;
EachComment: PROC [text: ROPE, node: Node, start, length, after: INT, hasLineBreak: BOOL] RETURNS [continue: BOOL] ~ {
The copyright line is assumed to have the format
Copyright <symbol> <year>, <year>, ..., <year> <some other text>
We fix up the <symbol> to be a true copyright symbol and add the current year to the end of the list if it's not already present.
index, foundYear, lastDigitPos: INT ¬ 0;
thisLooks: TextEdit.Looks ~ IF hasNodes THEN eLooks ELSE TextEdit.noLooks;
IF afterFirst=0 THEN {afterFirst ¬ after; firstHasLinebreak ¬ hasLineBreak};
MessageWindow.Append[text.Concat["||"]];
IF NOT Rope.IsPrefix["Copyright ", text] THEN
RETURN [TRUE];
index ¬ text.SkipTo[0, " \t"]; -- skip "Copyright" and the <symbol>
index ¬ text.SkipOver[index, " \t"];
index ¬ text.SkipTo[index, " \t"];
index ¬ text.SkipOver[index, " \t"];
lastDigitPos ¬ index-2; -- for malformed Copyright's, put year after symbol
WHILE index < length DO
c: CHAR ~ text.Fetch[index];
IF c IN ['0..'9] THEN {
lastDigitPos ¬ index;
index ¬ index + 1;
IF foundYear < 1000 THEN
foundYear ¬ foundYear * 10 + (c - '0)
ELSE
foundYear ¬ -1;
}
ELSE IF foundYear = year THEN
EXIT
ELSE IF c = ', OR c = ' OR c = '\t THEN {
foundYear ¬ 0;
index ¬ text.SkipOver[index + 1, ", \t"];
}
ELSE {
[] ¬ TextEdit.ReplaceByRope[root: root, dest: node,
start: start + lastDigitPos + 1, len: 0,
rope: IO.PutFR1[", %g", [integer[year]]],
event: TEditInput.CurrentEvent[]];
EXIT;
};
ENDLOOP;
IF text.Run[9, " c "] = 3 THEN -- replace obsolete math font symbol
[] ¬ TextEdit.ReplaceByRope[root: root, dest: node, rope: " Ó ", start: start + 9, len: 3, looks: thisLooks, event: TEditInput.CurrentEvent[]]
ELSE IF text.Run[pos1: 9, s2: " (C) ", case: FALSE] = 5 THEN
   -- replace legally useless "(C)" symbol
[] ¬ TextEdit.ReplaceByRope[root: root, dest: node, rope: " Ó ", start: start + 9, len: 5, looks: thisLooks, event: TEditInput.CurrentEvent[]]
ELSE IF hasNodes THEN {
looks: TextEdit.Looks ~ TextEdit.FetchLooks[node, start + 10];
IF NOT looks['e] THEN TextEdit.ChangeLooks[root: root, text: node, add: eLooks, start: start + 10, len: 1, event: TEditInput.CurrentEvent[]];
};
done ¬ TRUE;
RETURN [FALSE];
};
MessageWindow.Clear[];
[] ¬ EnumerateInitialCommentLines[root, EachComment];
IF NOT done THEN { -- We must add a new copyright line.
holder: ROPE ~ UserProfile.Line["Tioga.CopyrightHolder", "Xerox Corporation"];
line: ROPE ~ IO.PutFR["Copyright Ó %g by %g. All rights reserved.", [integer[year]], [rope[holder]]];
node: Node ~ TextNode.StepForward[root]; -- The first real node of the document
rope: ROPE ~ TextEditBogus.GetRope[node];
pa: PrefixAns ~ CommentPrefix[rope, 0];
prefix: ROPE ~ rope.Substr[len: pa.prefixLength];
index: INT ~ rope.SkipTo[skip: "\l\r"];
Add: PROC [node: Node, fullLine: ROPE, where: EditSpan.Place, comment: BOOL] ~ {
child: Node ~ EditSpan.InsertTextNode[root: root, old: node, where: where, event: TEditInput.CurrentEvent[]];
[] ¬ TextEdit.ReplaceByRope[root: root, dest: child, rope: fullLine, event: TEditInput.CurrentEvent[]];
IF comment THEN
TextEdit.PutComment[child, TRUE, TEditInput.CurrentEvent[]];
IF hasNodes THEN TextEdit.ChangeLooks[root: root, text: child, add: eLooks,
start: 10, len: 1, event: TEditInput.CurrentEvent[]];
IF node # root AND where # $before THEN
TextEdit.PutFormat[node: child, format: TextEdit.GetFormat[node],
event: TEditInput.CurrentEvent[]];
};
IF NOT IsComment[node] AND pa.prefixLength = 0 THEN -- no initial comment
Add[node, line, $before, TRUE]
ELSE IF firstHasLinebreak THEN { -- first comment ends with a newline
Add the line after the first comment, copying the comment characters and EOL character of the first line.
newline: CHAR ~ rope.Fetch[index];
fullLine: ROPE ~ MakeComment[line, prefix, pa].Concat[Rope.FromChar[newline]];
[] ¬ TextEdit.ReplaceByRope[root: root, dest: node, start: afterFirst, len: 0,
rope: fullLine, event: TEditInput.CurrentEvent[]];
}
ELSE IF pa.prefixLength = 0 THEN
Add[node, line, $child, TRUE]
ELSE
Add[node, MakeComment[line, prefix, pa], $after, IsComment[node]];
};
};
[] ¬ TEditLocks.Lock[root, "UpdateCopyrightNotice"];
Inner[! UNWIND => TEditLocks.Unlock[root]];
TEditLocks.Unlock[root];
RETURN [recordAtom: TRUE, quit: TRUE];
};
MakeComment: PROC [content, prefix: ROPE, pa: PrefixAns] RETURNS [ROPE] ~ {
IF pa.prefixLength=0 THEN RETURN [content]
ELSE IF pa.lineTerminable THEN RETURN IO.PutFR["%g %g", [rope[prefix]], [rope[content]] ]
ELSE RETURN IO.PutFR["%g %g %g", [rope[prefix]], [rope[content]], [rope[pa.closer]] ]};
UpdateLastEditedLine: TEditInput.CommandProc ~ {
[viewer: ViewerClasses.Viewer] RETURNS [recordAtom: BOOL ¬ TRUE, quit: BOOL ¬ FALSE]
root: Node ~ TiogaOps.ViewerDoc[viewer];
userName: ROPE ~ GetNiceUserName[];
editedBy: ROPE ~ UserProfile.Line["Tioga.LastEdited", userName.Concat[","]];
Inner: PROC ~ {
dateNode: Node ¬ NIL;
dateStart, dateLength: INT ¬ 0;
lastHasLineBreak: BOOL ¬ FALSE;
EachComment: PROC [text: ROPE, node: Node, start, length, after: INT, hasLineBreak: BOOL] RETURNS [continue: BOOL] ~ {
MessageWindow.Append[text.Concat["||"]];
lastHasLineBreak ¬ hasLineBreak;
IF text.Find[userName] >= 0 OR text.Find[editedBy] >= 0 THEN {
dStart, dLength: INT;
[dStart, dLength] ¬ FindDate[text];
IF dStart >= 0 THEN { -- We got one, Martha!
dateNode ¬ node;
dateStart ¬ dStart + start; -- offset in node as opposed to text
dateLength ¬ dLength;
};
};
RETURN [TRUE]; -- look for the last such node
};
lastCommentNode: Node;
lastCommentStart, lastCommentAfter: INT;
nowRope: ROPE ~ Convert.RopeFromTime[BasicTime.Now[]];
MessageWindow.Clear[];
[lastCommentNode, lastCommentStart, lastCommentAfter] ¬ EnumerateInitialCommentLines[root, EachComment];
IF dateNode # NIL THEN { -- Replace existing date
MessageWindow.Append[IO.PutFR["s: %g, l: %g", [integer[dateStart]], [integer[dateLength]]]];
[] ¬ TextEdit.ReplaceByRope[root: root, dest: dateNode, rope: nowRope, start: dateStart, len: dateLength, event: TEditInput.CurrentEvent[]];
}
ELSE IF lastCommentNode = NIL THEN-- No comments in document, so don't write a line
RETURN
ELSE { -- We must make a new line
rope: ROPE ~ TextEditBogus.GetRope[lastCommentNode];
eol: INT ~ rope.SkipTo[lastCommentStart, "\l\r"];
pa: PrefixAns ~ CommentPrefix[rope, lastCommentStart];
prefix: ROPE ~ rope.Substr[lastCommentStart, pa.prefixLength];
shortLine: ROPE ~ IO.PutFR["%g %g", [rope[editedBy]], [rope[nowRope]]];
prefixedLine: ROPE ~ MakeComment[shortLine, prefix, pa];
IF lastHasLineBreak THEN { -- line-break present so insert on new line
fullLine: ROPE ~ prefixedLine.Concat[Rope.FromChar[rope.Fetch[eol]]];
[] ¬ TextEdit.ReplaceByRope[root: root, dest: lastCommentNode, start: lastCommentAfter, len: 0, rope: fullLine, event: TEditInput.CurrentEvent[]];
}
ELSE { -- no line-break so make new line a new node
newNode: Node ~ EditSpan.InsertTextNode[root: root, old: lastCommentNode, where: $after, inherit: TRUE, event: TEditInput.CurrentEvent[]];
newNodeLocation: TextNode.Location ~ [node: newNode, where: TextNode.NodeItself];
newNodeSpan: TextNode.Span ~ [start: newNodeLocation, end: newNodeLocation];
[] ¬ TextEdit.ReplaceByRope[root: root, dest: newNode, rope: prefixedLine, event: TEditInput.CurrentEvent[]];
IF pa.prefixLength = 0 THEN
[] ¬ EditSpan.ChangeNesting[root: root, span: newNodeSpan, change: 2 - TextNode.Level[newNode], event: TEditInput.CurrentEvent[]];
};
};
};
[] ¬ TEditLocks.Lock[root, "UpdateLastEditedLine"];
Inner[! UNWIND => TEditLocks.Unlock[root]];
TEditLocks.Unlock[root];
RETURN [recordAtom: FALSE, quit: FALSE];
};
GetNiceUserName: PROC RETURNS [ROPE] = {
user: ROPE ¬ SystemNames.UserName[];
IF Rope.IsEmpty[user] THEN RETURN [user];
user ¬ Rope.Replace[user, 0, 1, Rope.FromChar[Ascii.Upper[Rope.Fetch[user, 0]]]];
RETURN [user];
};
monthList: LIST OF ROPE = LIST [
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
maxDateRope: NAT ~ 256;
In FindDate, lines that are longer than this length are assumed to NOT have embedded dates. This avoids searches of ludicrous length.
FindDate: PROC [rope: ROPE] RETURNS [start, length: INT] = {
Returns location in rope where a valid date occurs; [-1, -1] indicates no valid date.
len: INT = rope.Size[];
IF len <= maxDateRope THEN
FOR months: LIST OF ROPE ¬ monthList, months.rest UNTIL months = NIL DO
pos, end: INT ¬ 0;
WHILE pos < len DO
start ¬ rope.Find[months.first, pos];
IF start < 0 THEN EXIT; -- not this month
SELECT TRUE FROM
(end ¬ rope.Find["ST", start]) # -1 => {end ¬ end + 1};
(end ¬ rope.Find["DT", start]) # -1 => {end ¬ end + 1};
(end ¬ rope.Find["GMT", start]) # -1 => {end ¬ end + 2};
(end ¬ rope.Find[" am", start, FALSE]) # -1 => {end ¬ end + 2};
(end ¬ rope.Find[" pm", start, FALSE]) # -1 => {end ¬ end + 2};
ENDCASE => EXIT; -- no plausible ending!
[] ¬ Convert.TimeFromRope[rope.Substr[start, end - start + 1]
! Convert.Error--[reason: ErrorType, index: INT]-- => {
month may conflict with someone's name, so skip this and try further on
pos ¬ start + 3;
LOOP;}];
RETURN[start, end - start + 1];
ENDLOOP;
ENDLOOP;
RETURN[-1, -1];
};
InstallMenuButton: PROC [name: ROPE, proc: Menus.ClickProc] = {
old: Menus.MenuEntry =
Menus.FindEntry[menu: TiogaMenuOps.tiogaMenu, entryName: name];
new: Menus.MenuEntry =
Menus.CreateEntry[name: name, proc: proc, fork: FALSE];
IF old = NIL
THEN Menus.AppendMenuEntry[TiogaMenuOps.tiogaMenu, new]
ELSE Menus.ReplaceMenuEntry[TiogaMenuOps.tiogaMenu, old, new];
};
eLooks: TextEdit.Looks ¬ TextEdit.noLooks;
eLooks['e] ¬ TRUE;
InstallMenuButton["Ó", CopyrightButton];
TEditInput.Register[$UpdateCopyrightNotice, UpdateCopyrightNotice];
TEditInput.Register[$RedSave, UpdateLastEditedLine];
TEditInput.Register[$RedStore, UpdateLastEditedLine];
END.