<<>> <> <> <> <> <> <> <> 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] ~ { <> 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] ~ { <> 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 0 AND lc < lim DO IF lo0 THEN { IF lo0 THEN after ¬ fin ¬ lim} ELSE { fin ¬ contents.Index[begin, pa.closer]; IF finafter AND SkipOverBackward[contents, index-1, " \t"]>=after; <> 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] ~ { <> < , , ..., >> < 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}; <> IF NOT Rope.IsPrefix["Copyright ", text] THEN RETURN [TRUE]; index ¬ text.SkipTo[0, " \t"]; -- skip "Copyright" and the 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]; }; <> [] ¬ 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 <> 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] ~ { <> 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[]]; <> [lastCommentNode, lastCommentStart, lastCommentAfter] ¬ EnumerateInitialCommentLines[root, EachComment]; IF dateNode # NIL THEN { -- Replace existing date <> [] ¬ 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; <> FindDate: PROC [rope: ROPE] RETURNS [start, length: INT] = { <> 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]-- => { <> 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.