TEditDisplay2Impl.mesa
Copyright Ó 1985, 1986, 1987, 1988, 1991, 1992 by Xerox Corporation. All rights reserved.
Russ Atkinson (RRA) June 13, 1985 5:32:13 pm PDT
Michael Plass, March 2, 1987 10:39:29 pm PST
Willie-s, February 13, 1991 6:02 pm PST
Doug Wyatt, February 27, 1992 6:01 pm PST
DIRECTORY
Ascii USING [CR, LF],
CharOps USING [Blank],
EditNotify USING [AddNotifyProc, Change, ChangeSet],
NodeProps USING [Is],
Rope USING [Fetch, ROPE],
RopeReader USING [GetRopeReader, FreeRopeReader, Get, Ref, SetPosition],
TEditDisplay USING [],
TEditDocument USING [GetViewerForRoot, LineTable, maxClip, TEditDocumentData],
TEditInput USING [currentEvent],
TEditSelection USING [ComputePosLine, ComputeSpanLines, CannotFindIt],
TextEdit USING [Size],
TextNode USING [FirstChild, Forward, ForwardClipped, LastLocWithin, LastWithin, Level, Next, Parent, Root, StepForward],
Tioga USING [Node, Location, Span],
UndoEvent USING [Note, Event, EventRep, SubEvent],
ViewerClasses USING [Viewer],
ViewerOps USING [SetNewVersion];
TEditDisplay2Impl:
CEDAR
MONITOR
IMPORTS CharOps, EditNotify, NodeProps, Rope, RopeReader, TEditDocument, TEditInput, TEditSelection, TextEdit, TextNode, UndoEvent, ViewerOps
EXPORTS TEditDisplay, Tioga
= BEGIN
Node: TYPE ~ Tioga.Node;
Viewer: TYPE ~ ViewerClasses.Viewer;
Change: TYPE ~ EditNotify.Change;
Event: TYPE ~ UndoEvent.Event;
EventRep:
PUBLIC
TYPE ~ UndoEvent.EventRep;
SetNewVersion:
PROC [viewer: Viewer] = {
IF viewer #
NIL
THEN
WITH viewer.data
SELECT
FROM
tdd: TEditDocument.TEditDocumentData => {
IF tdd.tsInfo = NIL THEN ViewerOps.SetNewVersion[viewer];
};
ENDCASE => NULL;
};
EstablishLine:
PUBLIC
ENTRY
PROC [tdd: TEditDocument.TEditDocumentData, newLine: Tioga.Location, position:
INTEGER ¬ 0] = {
lines: TEditDocument.LineTable = tdd.lineTable;
IF lines #
NIL
THEN {
RRA: be careful here, just in case there is a race (one has been observed)
lines[position].pos ¬ newLine;
lines[position].valid ¬ FALSE;
};
};
UndoChangeViewer:
PROC [undoRef:
REF Change, currentEvent: Event] = {
WITH undoRef
SELECT
FROM
x:
REF Change.ChangingView => {
v: Viewer ¬ NARROW[x.view];
IF v #
NIL
THEN
WITH v.data
SELECT
FROM
tdd: TEditDocument.TEditDocumentData => {
IF
NOT AlreadySaved[v]
THEN {
lines: TEditDocument.LineTable = tdd.lineTable;
IF lines #
NIL
THEN
SaveViewerPos[v, tdd.lineTable[0].pos];
};
[] ¬ EstablishLine[tdd, x.old]
};
ENDCASE;
};
ENDCASE;
};
maxSearchDepth: NAT ¬ 20;
AlreadySaved:
PROC [v: Viewer]
RETURNS [
BOOL] = {
k: NAT ¬ maxSearchDepth;
IF TEditInput.currentEvent = NIL THEN RETURN [TRUE];
FOR l: UndoEvent.SubEvent ¬ TEditInput.currentEvent.subevents, l.next
UNTIL l=
NIL
DO
WITH l.undoRef
SELECT
FROM
x: REF Change.ChangingView => IF x.view = v THEN RETURN[TRUE];
ENDCASE;
IF (k ¬ k-1) = 0
THEN
EXIT;
Don't keep searching too long; give up and make a new event instead. - mfp
ENDLOOP;
RETURN [FALSE]
};
SaveViewerPos:
PROC [viewer: Viewer, oldLine: Tioga.Location] = {
notify: REF Change.ChangingView ¬ NEW[Change.ChangingView];
notify ¬ [ChangingView[viewer, oldLine]];
UndoEvent.Note[TEditInput.currentEvent, UndoChangeViewer, notify]
};
UpdateAfterTextEdit:
PROC [viewer: Viewer, text: Node, start, newlen, oldlen:
INT, oldrope: Rope.
ROPE] = {
invalidate lines containing old text and 1 line before
update line offsets for other lines in same node that start beyond the replacement
delta: INT = newlen-oldlen;
end: INT = start+oldlen;
Update:
PROC [v: Viewer] = {
tdd: TEditDocument.TEditDocumentData = NARROW[v.data];
lines: TEditDocument.LineTable = tdd.lineTable;
IF tdd = NIL THEN RETURN;
SetNewVersion[v];
IF NOT AlreadySaved[v] THEN SaveViewerPos[v, lines[0].pos]; -- record viewer start pos
FOR n:
INTEGER
DECREASING
IN [0..lines.lastLine]
DO
IF lines[n].pos.node = text
THEN {
where: INT ~ lines[n].pos.where;
IF lines[n].valid AND MAX[where, start] <= MIN[where+lines[n].nChars, end] THEN lines[n].valid ¬ FALSE;
SELECT where
FROM
<= start => {
IF n > 0
AND lines[n-1].pos.node=text
AND lines[n-1].valid
AND NOT lines[n].valid
AND
NOT (
SELECT Rope.Fetch[text.rope, where-1]
FROM
Ascii.CR, Ascii.LF => TRUE, ENDCASE => FALSE)
AND NOT LineContainsPreviousBlank[n, lines] THEN {
invalidate 1 line before start to take care of back ripple
lines[n-1].valid ¬ FALSE;
};
};
>= end => lines[n].pos.where ¬ where+delta;
ENDCASE => {
IF n=0
THEN {
lines[0].pos.where ¬ MIN[where, start+newlen];
tdd.fixStart ¬ TRUE
};
};
};
ENDLOOP;
};
LineContainsPreviousBlank:
PROC [n:
INTEGER, lines: TEditDocument.LineTable]
RETURNS [yes:
BOOL] = {
rdr: RopeReader.Ref;
lineStart: INT = lines[n].pos.where;
searchEnd: INT = MIN[lineStart+lines[n].nChars, start];
IF lineStart>=start THEN RETURN [FALSE];
rdr ¬ RopeReader.GetRopeReader[];
yes ¬ FALSE;
RopeReader.SetPosition[rdr, text.rope, lineStart];
FOR i:
INT ¬ lineStart, i+1
UNTIL i=searchEnd
DO
IF CharOps.Blank[RopeReader.Get[rdr]] THEN { yes ¬ TRUE; EXIT };
ENDLOOP;
RopeReader.FreeRopeReader[rdr]
};
Update[viewer];
FOR v: Viewer ¬ viewer.link, v.link
UNTIL v =
NIL
OR v=viewer
DO
Update[v];
ENDLOOP;
};
InvalidateSpan:
PUBLIC
PROC [viewer: Viewer, span: Tioga.Span, errorFlag:
BOOL ¬
TRUE, movingOut, unnesting:
BOOL ¬
FALSE] = {
invalidate lines containing text from nodes in span
InvalidateLinesInSpan:
PROC [v: Viewer] = {
start, end: INTEGER;
startClipped, endClipped: BOOL;
tdd: TEditDocument.TEditDocumentData = NARROW[v.data];
lines: TEditDocument.LineTable = tdd.lineTable;
IF tdd.movedOut
AND tdd.movedIn
THEN {
cannot trust the line table anymore so invalidate everything
start ¬ 0; end ¬ LAST[INTEGER];
startClipped ¬ endClipped ¬ FALSE
}
ELSE [start, end, startClipped, endClipped] ¬ TEditSelection.ComputeSpanLines[v, span !
TEditSelection.CannotFindIt => {
we must have previously deleted either first/last node in viewer
and span start/end node not in the viewer line table
end ¬ LAST[INTEGER];
startClipped ¬ endClipped ¬ FALSE;
start ¬ IF errorFlag THEN 0 --invalidate all-- ELSE LAST[INTEGER] --invalidate none--;
CONTINUE
} ];
IF end < 0 THEN RETURN; -- off screen
IF start = LAST[INTEGER] THEN
IF (unnesting AND tdd.clipLevel < TEditDocument.maxClip) OR
(lines[lines.lastLine].pos.node = span.start.node
AND
lines[lines.lastLine].pos.where+lines[lines.lastLine].nChars = span.start.where)
THEN start ¬ lines.lastLine
-- may be visible now
ELSE RETURN;
IF start < 0 THEN start ¬ 0; -- begins above screen
IF end =
LAST[
INTEGER]
THEN end ¬ tdd.lineTable.lastLine
-- ends below screen
ELSE IF endClipped AND (end > start OR NOT unnesting) THEN end ¬ end-1;
FOR n:
INTEGER
IN [start..end]
DO
tdd.lineTable.lines[n].valid ¬ FALSE;
ENDLOOP;
IF movingOut THEN tdd.movedOut ¬ TRUE
};
InvalidateLinesInSpan[viewer];
FOR v: Viewer ¬ viewer.link, v.link
UNTIL v =
NIL
OR v=viewer
DO
InvalidateLinesInSpan[v];
ENDLOOP
};
InvalidateChildren:
PUBLIC
PROC [viewer: Viewer, node: Node] = {
invalidate lines for children of node
child: Node;
span: Tioga.Span;
IF (child ¬ TextNode.FirstChild[node])=NIL THEN RETURN;
span ¬ [[child, 0], TextNode.LastLocWithin[node]];
InvalidateSpan[viewer, span]
};
InvalidateBranch:
PUBLIC
PROC [viewer: Viewer, node: Node] = {
invalidate lines for children of node
span: Tioga.Span ¬ [[node, 0], TextNode.LastLocWithin[node]];
InvalidateSpan[viewer, span]
};
InvalidateBranchAndNext:
PROC [viewer: Viewer, node: Node] = {
invalidate lines for children of node and one node beyond
last: Node = TextNode.LastWithin[node];
next: Node = TextNode.Forward[last].nx;
end: Node = IF next=NIL THEN last ELSE next;
span: Tioga.Span ¬ [[node, 0], [end, TextEdit.Size[end]]];
InvalidateSpan[viewer, span]
};
UpdateStartPos:
PROC [viewer: Viewer, first, last: Node] = {
this takes care of case in which something has been inserted in front of the first node
next: Node ¬ TextNode.StepForward[last];
UpdateViewerStartPos:
PROC [v: Viewer] = {
tdd: TEditDocument.TEditDocumentData = NARROW[v.data];
lines: TEditDocument.LineTable;
IF tdd = NIL THEN RETURN;
lines ¬ tdd.lineTable;
IF lines[0].pos.node # next THEN RETURN;
IF NOT AlreadySaved[v] THEN SaveViewerPos[v, lines[0].pos]; -- record viewer start pos
lines[0].pos ¬ [first, 0];
lines[0].valid ¬ FALSE
};
UpdateViewerStartPos[viewer];
FOR v: Viewer ¬ viewer.link, v.link
UNTIL v =
NIL
OR v=viewer
DO
UpdateViewerStartPos[v];
ENDLOOP;
};
InvalidateDest:
PROC [viewer: Viewer, node: Node, movingIn, afterDest:
BOOL] = {
InvalidateDestLine:
PROC [v: Viewer, dest: Tioga.Location] = {
line: INTEGER;
clipped, failed: BOOL ¬ FALSE;
WITH v.data
SELECT
FROM
tdd: TEditDocument.TEditDocumentData => {
IF movingIn THEN tdd.movedIn ¬ TRUE; -- remember that have moved nodes into this doc
[line, clipped] ¬ TEditSelection.ComputePosLine[v, dest !
TEditSelection.CannotFindIt => { failed ¬ TRUE; CONTINUE } ];
IF failed
AND tdd.clipLevel < TEditDocument.maxClip
THEN {
try adjusting for level clipping
n: Node ¬ dest.node;
delta: INTEGER ¬ TextNode.Level[n]-tdd.clipLevel;
IF delta > 0
THEN {
FOR i: INTEGER IN [0..delta) DO n ¬ TextNode.Parent[n]; ENDLOOP;
n ¬ TextNode.ForwardClipped[n, tdd.clipLevel].nx;
IF n #
NIL
THEN {
dest ¬ [n, 0];
afterDest ¬ failed ¬ FALSE;
[line, clipped] ¬ TEditSelection.ComputePosLine[v, dest
! TEditSelection.CannotFindIt => { failed ¬ TRUE; CONTINUE } ]
}
}
};
IF failed OR line < 0 THEN RETURN; -- not visible
IF line = LAST[INTEGER] THEN -- beyond last thing on viewer
IF tdd.clipLevel < TEditDocument.maxClip
THEN line ¬ tdd.lineTable.lastLine
ELSE RETURN;
IF afterDest AND NOT clipped AND line < tdd.lineTable.lastLine THEN line ¬ line+1;
tdd.lineTable[line].valid ¬ FALSE;
};
ENDCASE;
};
pos: Tioga.Location = [node,
IF afterDest THEN TextEdit.Size[node] ELSE 0];
InvalidateDestLine[viewer, pos];
FOR v: Viewer ¬ viewer.link, v.link
UNTIL v =
NIL
OR v=viewer
DO
InvalidateDestLine[v, pos];
ENDLOOP ;
};
FirstLevelParent:
PROC [node: Node]
RETURNS [Node] = {
prev1, prev2: Node;
UNTIL node=
NIL
DO
prev2 ¬ prev1;
node ¬ TextNode.Parent[prev1 ¬ node];
ENDLOOP;
RETURN [prev2]
};
SpanIsBranches:
PROC [first, last: Node]
RETURNS [
BOOL] = {
parent: Node = TextNode.Parent[first];
n: Node ¬ last;
IF TextNode.FirstChild[n] # NIL THEN RETURN [FALSE];
DO
SELECT (n ¬ TextNode.Parent[n])
FROM
parent => RETURN [TRUE];
NIL => EXIT;
ENDCASE;
IF TextNode.Next[n] # NIL THEN RETURN [TextNode.Parent[n] = parent];
ENDLOOP;
RETURN [FALSE]
};
SpanFromNodes:
PROC [first, last: Node]
RETURNS [Tioga.Span] = {
RETURN [[[first, 0], [last, MAX[TextEdit.Size[last], 1]-1]]]
};
ViewerFromNode:
PROC [node: Node]
RETURNS [Viewer] ~ {
IF node=NIL THEN RETURN[NIL];
RETURN[TEditDocument.GetViewerForRoot[TextNode.Root[node]]];
};
NotifyBeforeEdit:
PROC [change:
REF
READONLY Change] = {
viewer: Viewer;
span: Tioga.Span;
WITH change
SELECT
FROM
x:
REF
READONLY Change.NodeNesting => {
IF (viewer ¬ ViewerFromNode[x.first])=NIL THEN RETURN;
span ¬ SpanFromNodes[x.first, x.last];
IF NOT SpanIsBranches[x.first, x.last] THEN
span.end ¬ TextNode.LastLocWithin[FirstLevelParent[x.last]];
InvalidateSpan[viewer, span, TRUE, FALSE, (x.change<0)]
};
x:
REF
READONLY Change.MovingNodes => {
IF (viewer ¬ ViewerFromNode[x.first]) #
NIL
THEN {
branches: BOOL ¬ SpanIsBranches[x.first, x.last];
span ¬ SpanFromNodes[x.first, x.last];
IF NOT branches THEN span.end ¬ TextNode.LastLocWithin[FirstLevelParent[x.last]];
want span.last to be the last thing that can be influenced by the move
this is a conservative but easy to compute span that is sure to get everything
InvalidateSpan[viewer, span, TRUE, TRUE];
SetNewVersion[viewer]
};
x.dest.dirty ¬ x.pred.dirty ¬ TRUE;
IF (viewer ¬ ViewerFromNode[x.dest])=NIL THEN RETURN;
InvalidateDest[viewer, x.dest, TRUE, x.afterDest]
};
x:
REF
READONLY Change.ChangingProp => {
x.node.dirty ¬ TRUE;
IF (viewer ¬ ViewerFromNode[x.node])=NIL THEN RETURN;
IF NodeProps.Is[x.name, $Visible]
THEN {
This is a property that can affect the appearance
InvalidateBranchAndNext[viewer, x.node];
};
};
ENDCASE;
SetNewVersion[viewer];
};
NotifyAfterEdit:
PROC [change:
REF
READONLY Change] = {
viewer: Viewer;
span: Tioga.Span;
WITH change
SELECT
FROM
x:
REF
READONLY Change.ChangingText => {
x.text.dirty ¬ TRUE;
IF (viewer ¬ ViewerFromNode[x.text])=NIL THEN RETURN;
UpdateAfterTextEdit[viewer, x.text, x.start, x.newlen, x.oldlen, x.oldRope]
};
x:
REF
READONLY Change.InsertingNode => {
span: Tioga.Span;
child: Node;
DiffCommentProps: PROC [n1, n2: Node] RETURNS [BOOL] = {
RETURN [n1 = NIL OR n2 = NIL OR n1.comment # n2.comment]
};
x.new.new ¬ x.dest.dirty ¬ TRUE;
IF (viewer ¬ ViewerFromNode[x.dest])=NIL THEN RETURN;
SetNewVersion[viewer];
InvalidateDest[viewer, x.dest, FALSE, TRUE];
IF (child ¬ TextNode.FirstChild[x.new]) #
NIL
-- new inherited children of old
The following tests used to be done, but they can't be all inclusive since new Visible properties may be added. Therefore, invalidate children unconditionally. If this gets too expensive, we could perhaps conjure up a correct test.
AND (x.dest.next # x.new --different levels-- OR
x.dest.hasstyledef OR x.new.hasstyledef OR
x.dest.hasprefix OR x.new.hasprefix OR
x.dest.haspostfix OR x.new.haspostfix OR
x.dest.hascharprops OR x.new.hascharprops OR
x.dest.formatName # x.new.formatName OR
DiffCommentProps[x.dest, x.new])
THEN {
-- must invalidate children too
span ¬ [[child, 0], TextNode.LastLocWithin[x.new]];
InvalidateSpan[viewer, span];
UpdateStartPos[viewer, x.new, x.new]
}
};
x:
REF
READONLY Change.MovingNodes => {
IF (viewer ¬ ViewerFromNode[x.dest])=NIL THEN RETURN;
UpdateStartPos[viewer, x.first, x.last];
IF SpanIsBranches[x.first, x.last] THEN RETURN;
span.start ¬ [x.dest, 0];
span.end ¬ TextNode.LastLocWithin[FirstLevelParent[x.last]];
InvalidateSpan[viewer, span]
};
ENDCASE;
};
Init:
PROC = {
beforeEditChanges: EditNotify.ChangeSet ¬ ALL[TRUE];
afterEditChanges: EditNotify.ChangeSet ¬ ALL[FALSE];
beforeEditChanges[ChangingText] ¬ FALSE;
beforeEditChanges[InsertingNode] ¬ FALSE;
EditNotify.AddNotifyProc[proc: NotifyBeforeEdit, time: before, changeSet: beforeEditChanges];
afterEditChanges[MovingNodes] ¬ TRUE;
afterEditChanges[InsertingNode] ¬ TRUE;
afterEditChanges[ChangingText] ¬ TRUE;
EditNotify.AddNotifyProc[proc: NotifyAfterEdit, time: after, changeSet: afterEditChanges];
};
Init[];
END.