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];
~
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;
};
}
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
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];