<> <> <> <> <> <> <> <<>> 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 <<>> <> <<>> Location: TYPE = TiogaOps.Location; ROPE: TYPE = Rope.ROPE; STREAM: TYPE = IO.STREAM; TiogaRef: TYPE = TiogaOps.Ref; Viewer: TYPE = ViewerClasses.Viewer; <<>> <> <<>> LogChange: ENTRY PROC [name: ROPE, description: ROPE] = { ENABLE UNWIND => NULL; outName: ROPE _ UserProfile.Token["EditorComforts.ChangesLog", NIL]; IF outName # NIL THEN { <> 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]; <> 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 => {}; }; }; <<>> <> <<>> 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 <> 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] # '[ => <> TiogaOps.BackSpace[2]; Rope.Fetch[r, loc.where] = ', => <> 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]; }; <<>> <> 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[]; <> 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 => { <> start _ start + 3; LOOP}]; RETURN[i, j]; ENDLOOP; ENDLOOP; RETURN[-1, -1]; }; SelectDate: PROC [viewer: Viewer, loc: Location, rope: ROPE] RETURNS [BOOL] = { <> 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 <> SELECT TRUE FROM node = NIL => { <> node _ TiogaOps.FirstChild[rootNode]; nodeRope _ TiogaOps.GetRope[node]; TiogaOps.SelectPoint[viewer: viewer, caret: [node: node, where: 0]]; next _ -1; }; next = -1 => { <> 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 => { <> 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 { <> 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 { <> TiogaOps.SaveSelB[]; foundOne _ TRUE; }; IF ~foundOne AND Rope.Find[s1: r, s2: "Last Edited by:"] # -1 THEN newInsertAtNode _ foundLastEditedNode _ TRUE; ENDLOOP; IF foundOne THEN { <> TiogaOps.RestoreSelB[]; TiogaOps.Delete[]; TiogaOps.InsertTime[]; GO TO done; }; <> 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 { <> ForceToLevel[1, TRUE]; TiogaOps.InsertRope[GenLastEdited[]]; } ELSE { <> 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: 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 { <> 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; }; <
> <> WITH TiogaOps.GetProp[n: rootNode, name: $ChangeLog] SELECT FROM entry: REF ChangeLogEntry => currentChangeLogEntry _ entry; ENDCASE; SELECT TRUE FROM mergeWithPrevious = newSession AND currentChangeLogEntry # NIL => { <> 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 { <> FindLastChangeLog: PROC RETURNS [TiogaRef _ NIL] = { n: TiogaRef _ end.node; r: ROPE _ NIL; <> 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; <> createNew => { <> 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 => <> 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 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 { <> 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 <> 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; <> 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 { <> IF currentChangeLogEntry # NIL THEN { currentChangeLogEntry.changed _ changed; changeLogNode _ currentChangeLogEntry.node; -- for return value IF NOT insertPlaceHolders THEN DeletePlaceHolders[]; }; RETURN; }; IF currentChangeLogEntry # NIL THEN { <> 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 { <> 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 => { <> 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: BOOL _ TRUE; 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 => { <> pos: INT _ copyPos+9; IF Rope.Run[contents, pos, " c "]=3 THEN { <> 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"]]; <> IF Rope.Find[contents, year, pos] < 0 THEN { <> 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 { <> 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; <> 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 { <> LOOP}; empty _ FALSE; TiogaOps.SelectNodes[viewer, node, node]; EXIT; ENDLOOP; IF empty THEN TiogaOps.SelectNodes[viewer, root, root]; IF insertPlain THEN { <> 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 { <> 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; }; }; <<>> <> 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; <> 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]; }; <> <> maxNodesToSearch: INT _ 6; minNodesForFormat: INT _ 3; nodeFormat: ROPE _ "code"; maxDateRope: NAT _ 256; <> Init[]; END. <> <> <> <> <> <> <<>> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <<>> <> <> <<>> <> <> <> <<>> <> <> <<>> <> <> <<>> <> <> <> <> <<>>