-- file: EditorDisplay.Mesa
-- last edited by Brotz, November 13, 1981 12:30 PM

DIRECTORY
Ascii USING [CR, SP, TAB],
displayCommon USING [charPropertyTable, mcFont],
dsD: FROM "DisplayDefs" USING [BbtPtr, BitBlt, ChangeCursor, charBbtPtr, CharFontPtr,
CharProperty, ClearRectangle, CursorShape, GetCharBreakProp, GetCharProperty,
GetCharRightX, GetCursor, GetStaticCharWidth, GrayShade, InvertRectangle,
lineHeight, MoveFullWidthRectangleVertically, PaintPicture, PutStringInBitMap,
ScreenXCoord, ScreenYCoord, SlideFullWidthRectangleVertically,
SlideRectangleHorizontally, xOrigin, yOrigin],
Editor USING [UnderlineType],
exD: FROM "ExceptionDefs" USING [SysBug],
FrameDefs USING [GlobalFrame],
FrameOps USING [Start],
inD: FROM "InteractorDefs" USING [CharIndex, CRWidth, leftMargin, LinePtr,
MessageTextNbrPtr, MoveDMCMBoundary, NbrPtr, rightMargin, ScreenXCoord,
ScreenYCoord, TextSelection, TextSelectionPtr, ThumbLineNbrPtr],
Inline USING [BITAND, BITOR, LongDiv, LongMult],
intCommon USING [cmTextNbr, dmcmBoundaryPadNbr, source, target],
ovD: FROM "OverviewDefs" USING [CharMask, LineBreakValue],
prD: FROM "ProtectionDefs" USING [AdjustProtFields],
Storage USING [Node],
vmD: FROM "VirtualMgrDefs" USING [CharCache, CharIndex, DisplayMessage,
GetMessageChar, GetMessageSize, PutMessageChar, UnlockTOC, VirtualMessagePtr];

EditorDisplay: PROGRAM
IMPORTS disC: displayCommon, dsD, exD, FrameDefs, FrameOps, inD, Inline,
intC: intCommon, prD, Storage, vmD
EXPORTS Editor, inD
SHARES dsD =

BEGIN
OPEN Editor, inD;

-- Smart Display Section of the Editor Department
-- Maintains lines and characters within lines on the screen. Handles scrolling,
-- refreshing, mapping x,y to character indices.


-- Global variables of the smart display


charBbtPtr: dsD.BbtPtr;


RefreshFromFirstChange: PUBLIC PROCEDURE [actionIndex: CharIndex,
deletedChars, insertedChars: CARDINAL, mnp: MessageTextNbrPtr] =
-- A change in the message text has occured at actionPoint. Its relation to the screen is
-- unknown. This routine is to fix up the screen to reflect the new state of the message
-- text.
BEGIN
nextIndex, prevIndex, previousLineStartIndex, prevLineLastIndex: CharIndex;
previousLine, startLine: LinePtr;
rightX: ScreenXCoord;
normalChar: CHARACTER;
message: vmD.VirtualMessagePtr ← mnp.message;
endChanged: BOOLEAN;

AffectsPreviousLine: PROCEDURE RETURNS [affected: BOOLEAN] =
-- Determines whether a change at actionIndex has affected the previous line. If so,
-- affected is set to true. Line may be a visible line or the firstLineOffScreen.
-- previousLine NIL if off top or nonexistent. If affected, rightX of previous line is
-- computed whether previous line is on screen or not.
BEGIN
char: CHARACTER;
firstUnitEndIndex: CharIndex;
thisLineFirstCharIndex: CharIndex ← startLine.firstCharIndex;
message: vmD.VirtualMessagePtr ← mnp.message;
newRightX: ScreenXCoord;

affected ← FALSE;
IF thisLineFirstCharIndex = 0
OR (char ← vmD.GetMessageChar[message, thisLineFirstCharIndex - 1]) = Ascii.CR
THEN RETURN;
IF (previousLine ← FindLineBeforePlace[mnp.lines, startLine]) = NIL THEN
BEGIN
message.formatEnd ← MIN[message.formatEnd, thisLineFirstCharIndex];
previousLineStartIndex
← SearchBackForFirstCharIndexOfLine[thisLineFirstCharIndex - 1, message];
rightX ← RangeRightXInLine
[previousLineStartIndex, thisLineFirstCharIndex, inD.leftMargin, message];
END
ELSE BEGIN
rightX ← previousLine.rightX;
previousLineStartIndex ← previousLine.firstCharIndex;
END;
IF dsD.GetCharProperty[char, alphaNumeric] THEN
BEGIN -- previous line was broken at a long word
IF thisLineFirstCharIndex < actionIndex THEN RETURN;
IF thisLineFirstCharIndex >= vmD.GetMessageSize[message] THEN
newRightX ← rightX
ELSE newRightX ← dsD.GetCharRightX
[vmD.GetMessageChar[message, thisLineFirstCharIndex], rightX];
END
ELSE BEGIN
[newRightX, firstUnitEndIndex] ← NextUnit[message, thisLineFirstCharIndex, rightX];
IF firstUnitEndIndex < actionIndex THEN RETURN;
END;
affected ← (newRightX <= inD.rightMargin)
OR (thisLineFirstCharIndex < vmD.GetMessageSize[message]
AND dsD.GetCharProperty
[vmD.GetMessageChar[message, thisLineFirstCharIndex], white]);
END; -- of AffectsPreviousLine --

ReformatWithinLine: PROCEDURE RETURNS [endChanged: BOOLEAN] =
BEGIN
trailingBlockStartX, rightX, insertionRightX, insertionLimit: ScreenXCoord;
trailingBlockXLength, deletedXLength: CARDINAL;
i, trailingBlockStartIndex, trailingBlockEndIndex, nextUnitEndIndex: CharIndex;
deltaX: INTEGER;
char: CHARACTER;
get: POINTER TO vmD.CharCache ← @message.get;
actionX: ScreenXCoord = MapCharIndexInLineToLeftX[actionIndex, startLine, message];

BEGIN -- for EXITS --
endChanged ← TRUE; -- guilty until proven otherwise.
IF actionIndex + deletedChars >= startLine.nextLine.firstCharIndex THEN
BEGIN -- deletion removed rest of line.
startLine.state ← normalText; -- if inserting at end of message, may have been endOfMessage.
GO TO GiveUp;
END;
-- some of line remains
-- First, find how much space was deleted
trailingBlockXLength ← 0;
trailingBlockStartIndex ← actionIndex + insertedChars;
trailingBlockEndIndex ← startLine.nextLine.firstCharIndex - deletedChars + insertedChars;
FOR i IN [trailingBlockStartIndex .. trailingBlockEndIndex) DO
char ← IF i IN [get.first .. get.free)
THEN message.buffer.chars[i + get.floor - get.first]
ELSE vmD.GetMessageChar[message, i];
IF Inline.BITAND[char, ovD.CharMask] = Ascii.TAB THEN GO TO GiveUp
ELSE trailingBlockXLength ← trailingBlockXLength + dsD.GetStaticCharWidth[char];
ENDLOOP;
deletedXLength ← startLine.rightX - trailingBlockXLength - actionX;
-- Check if insertion will fit on this line.
insertionLimit ← inD.rightMargin - startLine.rightX + deletedXLength + actionX;
insertionRightX ← actionX;
FOR i IN [actionIndex .. trailingBlockStartIndex) DO
char ← IF i IN [get.first .. get.free)
THEN message.buffer.chars[i + get.floor - get.first]
ELSE vmD.GetMessageChar[message, i];
IF char >= ovD.LineBreakValue THEN
vmD.PutMessageChar[message, i, char ← Inline.BITAND[char, ovD.CharMask]];
insertionRightX ← dsD.GetCharRightX[char, insertionRightX];
IF insertionRightX > insertionLimit OR char = Ascii.CR THEN GO TO GiveUp;
ENDLOOP;
IF insertionLimit < insertionRightX + inD.CRWidth
AND trailingBlockEndIndex > 0
AND ~dsD.GetCharProperty
[vmD.GetMessageChar[message, trailingBlockEndIndex - 1], white]
THEN GO TO GiveUp;

-- Entire trailing block will remain on this line. Rearrange screen so far.
trailingBlockStartX ← startLine.rightX - trailingBlockXLength;
deltaX ← insertionRightX - actionX - deletedXLength;
dsD.SlideRectangleHorizontally
[trailingBlockStartX, startLine.rightX, startLine.y, startLine.nextLine.y, deltaX];
dsD.ClearRectangle[actionX, trailingBlockStartX + deltaX, startLine.y, startLine.nextLine.y];
[ , ] ← CharacterBlaster[actionIndex, trailingBlockStartIndex, message, actionX, startLine.y];

-- Will text move up from next line?
[rightX, nextUnitEndIndex] ← NextUnit
[message, trailingBlockEndIndex, startLine.rightX + deltaX];
IF deltaX < 0
AND (char ← vmD.GetMessageChar[message, trailingBlockEndIndex - 1]) # Ascii.CR
AND (~dsD.GetCharProperty[char, white] OR rightX < inD.rightMargin
OR (trailingBlockEndIndex < vmD.GetMessageSize[message]
AND dsD.GetCharProperty
[vmD.GetMessageChar[message, trailingBlockEndIndex], white]))
THEN [nextIndex, startLine.rightX] ← FormatAndDisplaySuffix
[mnp, trailingBlockEndIndex, startLine.firstCharIndex, startLine.rightX + deltaX, startLine]
ELSE BEGIN
-- text will not move up, the buck stops here.
nextIndex ← trailingBlockEndIndex;
startLine.rightX ← startLine.rightX + deltaX;
endChanged ← FALSE;
END;
EXITS
GiveUp =>
BEGIN
-- line will definitely terminate at a different character than before.
dsD.ClearRectangle[actionX, startLine.rightX, startLine.y, startLine.nextLine.y];
[nextIndex, startLine.rightX] ← FormatAndDisplaySuffix
[mnp, actionIndex, startLine.firstCharIndex, actionX, startLine];
END;
END;
END; -- of ReformatWithinLine --

ReformatScreenLines: PROCEDURE [startIndex: CharIndex] =
-- startIndex will be the first char index on startLine. A change of insertedChars and
-- deletedChars has occurred at actionIndex. Reformat and redisplay the minimum
-- number of screen lines from startLine down so that all lines are properly formatted.
-- Any lines whose bitmaps are reusable in their present form should be reused.
BEGIN
line, startCopyLine, endOfTextLine, firstLineNotCopied, endCopyLine, usableLine,
lineToReformat: LinePtr;
moveEOM: BOOLEAN ← FALSE;
correctedIndex: CharIndex;
insertionEnd: CharIndex = actionIndex + insertedChars;
deltaChars: INTEGER = insertedChars - deletedChars;
message: vmD.VirtualMessagePtr = mnp.message;
messageLength: CharIndex = vmD.GetMessageSize[message];
firstLineOffScreen: LinePtr = mnp.firstLineOffScreen;

PaintNewLines: PROCEDURE [startLine, endLine: LinePtr] =
BEGIN
FOR line: LinePtr ← startLine, line.nextLine UNTIL line = endLine DO
ClearLine[line];
[line.rightX, ] ← CharacterBlaster
[line.pendingIndex, line.nextLine.pendingIndex, message, leftMargin, line.y];
line.firstCharIndex ← line.pendingIndex;
line.state ← normalText;
ENDLOOP;
END; -- of PaintNewLines --

-- find first line that may be usable in its present form.
FOR usableLine ← startLine, usableLine.nextLine
UNTIL usableLine = firstLineOffScreen DO
IF actionIndex + deletedChars <= usableLine.firstCharIndex THEN EXIT;
ENDLOOP;
lineToReformat ← startLine;
DO -- decide which lines will be copied, which lines will be reformatted.
correctedIndex ← usableLine.firstCharIndex + deltaChars;
SELECT TRUE FROM
(lineToReformat = firstLineOffScreen) =>
BEGIN
lineToReformat.pendingIndex ← startIndex;
startCopyLine ← endCopyLine ← endOfTextLine ← firstLineOffScreen;
firstLineNotCopied ← usableLine;
EXIT;
END;
(usableLine = firstLineOffScreen OR startIndex < correctedIndex) =>
BEGIN -- a line must be reformatted as no usable line exists for this startIndex.
lineToReformat.pendingIndex ← startIndex;
IF startIndex = messageLength THEN
BEGIN
-- there will be no copying, must paint the End of message indicator.
startCopyLine ← endCopyLine ← usableLine ← firstLineNotCopied ← endOfTextLine
← lineToReformat;
EXIT;
END;
lineToReformat ← lineToReformat.nextLine;
[startIndex, , ] ← FormatLine[startIndex, message];
END;
(startIndex > correctedIndex) =>
-- current usableLine is not usable after all.
usableLine ← usableLine.nextLine;
(startIndex = correctedIndex) =>
BEGIN
-- all lines from usableLine on are good. Move as many lines as we can.
startCopyLine ← lineToReformat;
firstLineNotCopied ← usableLine;
UNTIL lineToReformat = firstLineOffScreen
OR firstLineNotCopied = firstLineOffScreen
OR firstLineNotCopied.state = trailingBlankLine DO
lineToReformat.pendingIndex ← firstLineNotCopied.firstCharIndex + deltaChars;
lineToReformat ← lineToReformat.nextLine;
IF firstLineNotCopied.state = endOfMessage THEN moveEOM ← TRUE;
firstLineNotCopied ← firstLineNotCopied.nextLine;
ENDLOOP;
endCopyLine ← lineToReformat;
-- Reformat remainder of lines.
startIndex ← firstLineNotCopied.firstCharIndex - deletedChars + insertedChars;
UNTIL lineToReformat = firstLineOffScreen OR startIndex = messageLength DO
lineToReformat.pendingIndex ← startIndex;
[startIndex, , ] ← FormatLine[startIndex, message];
lineToReformat ← lineToReformat.nextLine;
ENDLOOP;
lineToReformat.pendingIndex ← startIndex;
endOfTextLine ← lineToReformat;
EXIT;
END;
ENDCASE;
ENDLOOP;
-- now rearrange screen according to previous decisions.
dsD.MoveFullWidthRectangleVertically
[top: usableLine.y, bottom: firstLineNotCopied.y, newTop: startCopyLine.y];
-- now move the rightXs of these same lines
IF startCopyLine.y <= usableLine.y THEN --moving up--
MoveRightXs[fromStart: usableLine, fromEnd: firstLineNotCopied, toStart: startCopyLine]
ELSE --moving down--
BEGIN
DestructiveReverse[usableLine];
MoveRightXs
[fromStart: firstLineNotCopied.nextLine, fromEnd: NIL, toStart: endCopyLine.nextLine];
DestructiveReverse[firstLineOffScreen];
END;
PaintNewLines[startLine, startCopyLine];
FOR line ← startCopyLine, line.nextLine UNTIL line = endCopyLine DO
line.state ← IF (line.firstCharIndex ← line.pendingIndex) = messageLength
THEN endOfMessage ELSE normalText;
ENDLOOP;
PaintNewLines[endCopyLine, endOfTextLine];
-- We’ve painted all the text that there is. Now clean up with possible "End of message", trailing blank lines, and fix up mnp.firstLineOffScreen.
IF endOfTextLine = firstLineOffScreen THEN
BEGIN
endOfTextLine.firstCharIndex ← endOfTextLine.pendingIndex;
endOfTextLine.state ← SELECT TRUE FROM
endOfTextLine.pendingIndex # messageLength => normalText,
moveEOM => trailingBlankLine,
ENDCASE => endOfMessage;
END
ELSE BEGIN
IF ~moveEOM THEN
BEGIN
PaintEndOfMessageLine[mnp, endOfTextLine];
endOfTextLine ← endOfTextLine.nextLine;
END;
ClearScreenLinesToTrailingBlankLines[endOfTextLine, messageLength, mnp];
END;
END; -- of ReformatScreenLines --


IF deletedChars = 0 AND insertedChars = 0 THEN RETURN;

IF mnp.protectedFieldPtr#NIL THEN
prD.AdjustProtFields[pfpp: @mnp.protectedFieldPtr, actionIndex: actionIndex,
deletedChars: deletedChars, insertedChars: insertedChars];

startLine ← MapCharIndexToLine[actionIndex, mnp];
IF startLine = NIL THEN
BEGIN
prevIndex ← actionIndex;
message.formatEnd ← MIN[message.formatEnd, prevIndex];
THROUGH [1 .. 2] DO
IF prevIndex = 0 THEN EXIT;
prevIndex ← SearchBackForFirstCharIndexOfLine[prevIndex - 1, message];
ENDLOOP;
-- Refresh without painting underlines.
RefreshToPlaceCharOnLine[actionIndex, mnp.lines, mnp, FALSE];
RETURN;
END;
IF AffectsPreviousLine[] THEN
BEGIN
prevLineLastIndex ← startLine.firstCharIndex - 1;
normalChar ←
Inline.BITAND[vmD.GetMessageChar[message, prevLineLastIndex], ovD.CharMask];
vmD.PutMessageChar[message, prevLineLastIndex, normalChar];
IF previousLine # NIL THEN -- previousLine is on screen --
[nextIndex, previousLine.rightX] ← FormatAndDisplaySuffix
[mnp, startLine.firstCharIndex, previousLineStartIndex, rightX, previousLine]
ELSE -- previousLine is not visible --
[nextIndex, , ] ← FormatLine[previousLineStartIndex, message];
ClearLine[startLine];
startLine.firstCharIndex ← nextIndex;
startLine.state ← normalText;
[nextIndex, startLine.rightX]
← FormatAndDisplaySuffix[mnp, nextIndex, nextIndex, inD.leftMargin, startLine];
endChanged ← TRUE;
END
ELSE -- previous line not affected --
endChanged ← ReformatWithinLine[];
IF endChanged THEN
BEGIN
IF startLine.firstCharIndex # vmD.GetMessageSize[message]
THEN startLine ← startLine.nextLine;
ReformatScreenLines[nextIndex];
END
ELSE AdjustFirstCharIndices[startLine.nextLine, insertedChars - deletedChars];
message.formatEnd ← mnp.firstLineOffScreen.firstCharIndex;
[] ← MakeCharIndexVisible[actionIndex, mnp];
END; -- of RefreshFromFirstChange --


CharacterBlaster: PROCEDURE [startIndex, endIndex: CharIndex,
message: vmD.VirtualMessagePtr, startX: ScreenXCoord, topY: ScreenYCoord]
RETURNS [nextX: ScreenXCoord, nextIndex: CharIndex] =
-- Blasts characters [startIndex, endIndex) on the screen starting at [startX, topY] as fast as
-- possible. Will stop blasting out characters if a PCR bit or CR is seen.
BEGIN
i: CharIndex;
x, y: CARDINAL;
char: CHARACTER;
offsetPtr: POINTER;
charFontPtr: dsD.CharFontPtr;
get: POINTER TO vmD.CharCache ← @message.get;
endOfLine: BOOLEAN ← FALSE;
-- set x, y to bitmap relative coordinates.
x ← startX - dsD.xOrigin;
y ← topY - dsD.yOrigin;
FOR i IN [startIndex .. endIndex) DO
char ← IF i IN [get.first .. get.free)
THEN message.buffer.chars[i + get.floor - get.first]
ELSE vmD.GetMessageChar[message, i];
IF char >= ovD.LineBreakValue OR char = Ascii.CR THEN
{endOfLine ← TRUE; char ← Inline.BITAND[char, ovD.CharMask]};
SELECT char FROM
Ascii.SP, Ascii.TAB, Ascii.CR =>
x ← dsD.GetCharRightX[char, x + dsD.xOrigin] - dsD.xOrigin;
ENDCASE =>
BEGIN
offsetPtr ← disC.mcFont + LOOPHOLE[char, CARDINAL];
charFontPtr ← offsetPtr + offsetPtr↑;
x ← (charBbtPtr.dlx ← x) + (charBbtPtr.dw ← charFontPtr.width);
charBbtPtr.dty ← y + charFontPtr.ySkip;
charBbtPtr.dh ← charFontPtr.height;
IF charBbtPtr.ptrs = short THEN charBbtPtr.sbca ← charFontPtr - charFontPtr.height
ELSE charBbtPtr.slbca ← charFontPtr - charFontPtr.height;
dsD.BitBlt[charBbtPtr];
END;
IF endOfLine THEN {endIndex ← i + 1; EXIT};
ENDLOOP;
RETURN[x + dsD.xOrigin, endIndex];
END; -- of CharacterBlaster --


MoveRightXs: PROCEDURE [fromStart, fromEnd, toStart: LinePtr] =
-- Moves rightX fields of specified line range
BEGIN
FOR fromStart ← fromStart, fromStart.nextLine UNTIL fromStart = fromEnd DO
toStart.rightX ← fromStart.rightX;
toStart ← toStart.nextLine;
ENDLOOP;
END; -- of MoveRightXs --


RefreshToPlaceCharOnLine: PUBLIC PROCEDURE [startCharIndex: CharIndex,
line: LinePtr, mnp: MessageTextNbrPtr, underline: BOOLEAN ← TRUE] =
-- Refresh CM window from line on (inclusive), so that firstCharIndex appears somewhere
-- in line after refresh. Selections will be refreshed with their underlines iff underline
-- is TRUE.
BEGIN
-- Advance highwater mark past startCharIndex to insure that head of message is properly
-- formatted. Find the index of the first character on the line which contains
-- startCharIndex. Refresh the screen.
firstCharIndex: CharIndex
← SearchBackForFirstCharIndexOfLine[startCharIndex, mnp.message];
RefreshSoThatFirstCharStartsLine[firstCharIndex, line, mnp, underline];
END; -- of RefreshToPlaceCharOnLine --


RefreshSoThatFirstCharStartsLine: PUBLIC PROCEDURE [firstChar: CharIndex,
firstLine: LinePtr, mnp: MessageTextNbrPtr, underline: BOOLEAN ← TRUE] =
-- Refresh all lines of screen in range [firstLine .. bottomLine], so that firstChar starts
-- firstLine. Selections will be refreshed with their underlines iff underline is TRUE.
-- Side effects: advances message.formatEnd.
BEGIN
message: vmD.VirtualMessagePtr ← mnp.message;
messageLength: CharIndex;
line: LinePtr;

IF ~mnp.haveMessage THEN RETURN;
messageLength ← vmD.GetMessageSize[message];
dsD.ClearRectangle[leftMargin, rightMargin, firstLine.y, mnp.bottomY];
FOR line ← firstLine, line.nextLine DO
line.firstCharIndex ← firstChar;
IF (line = mnp.firstLineOffScreen) THEN GOTO ScreenHasNowBeenFilled;
IF (firstChar = messageLength) THEN GOTO TrailingScreenLinesRemain;
line.state ← normalText;
IF firstChar >= message.formatEnd THEN [ , , ] ← FormatLine[firstChar, message];
[firstChar, , ] ← RompAndStomp[line, firstChar, mnp, underline]
REPEAT
ScreenHasNowBeenFilled =>
BEGIN
line.state ← IF firstChar = messageLength THEN endOfMessage ELSE normalText;
line.firstCharIndex ← firstChar;
END;
TrailingScreenLinesRemain =>
BEGIN
line.rightX ← leftMargin;
PaintEndOfMessageLine[mnp, line];
ClearScreenLinesToTrailingBlankLines
[startLine: line.nextLine, charIndex: messageLength, mnp: mnp];
END;
ENDLOOP;
message.formatEnd ← MAX[firstChar, message.formatEnd];
END; -- of RefreshSoThatFirstCharStartsLine --


RefreshEndOfMessage: PUBLIC PROCEDURE [mnp: MessageTextNbrPtr] =
-- Repaints "End of Message" line if visible with current endOfMessageString.
BEGIN
FOR line: LinePtr ← mnp.lines, line.nextLine UNTIL line = mnp.firstLineOffScreen DO
IF line.state = endOfMessage THEN {PaintEndOfMessageLine[mnp, line]; RETURN};
ENDLOOP;
END; -- of RefreshEndOfMessage --


PaintEndOfMessageLine: PROCEDURE [mnp: MessageTextNbrPtr, line: LinePtr] =
BEGIN
line.state ← endOfMessage;
line.firstCharIndex ← vmD.GetMessageSize[mnp.message];
ClearLine[line];
line.rightX ← dsD.PutStringInBitMap[inD.leftMargin, line.y, mnp.endString, italicFace];
END; -- of PaintEndOfMessageLine --


MapXInLineToCharIndex: PUBLIC PROCEDURE [x: ScreenXCoord, line: LinePtr,
message: vmD.VirtualMessagePtr]
RETURNS[index: CharIndex, leftX, rightX: ScreenXCoord] =
-- Returns the charIndex covering x in line and returns the x bounds of that charIndex.
BEGIN
-- If x is to right of rightmost character on line, will return info for rightmost character.
-- If line is empty, will return charIndex for next character in message from point given
-- and will return empty x interval.
i: CharIndex;
get: POINTER TO vmD.CharCache ← @message.get;
leftX ← rightX ← inD.leftMargin;
index ← line.firstCharIndex;
FOR i IN [index .. line.nextLine.firstCharIndex) DO
leftX ← rightX;
index ← i;
rightX ← dsD.GetCharRightX[IF index IN [get.first .. get.free)
THEN message.buffer.chars[index + get.floor - get.first]
ELSE vmD.GetMessageChar[message, index], rightX];
IF x < rightX THEN RETURN;
ENDLOOP;
END; -- of MapXInLineToCharIndex --


ScrollUpMessageNbr: PUBLIC PROCEDURE [y: ScreenYCoord, mnp: MessageTextNbrPtr] =
-- y is the ScreenYCoord of the cursor. Scrolls the screen Up, based on this y position.
-- Adjusts all structures to reflect the new screen contents.
BEGIN
oldTopLine, newBottomScreenLine, lastLineOnScreen: LinePtr;
messageLength: CharIndex ← vmD.GetMessageSize[mnp.message];
firstLineOffScreen: LinePtr = mnp.firstLineOffScreen;

oldTopLine ← MapYToNonBlankLine[y, mnp];
IF oldTopLine = mnp.lines THEN
IF oldTopLine.state = endOfMessage THEN RETURN
ELSE oldTopLine ← oldTopLine.nextLine;
newBottomScreenLine ←
MoveLines[top: oldTopLine, bottom: firstLineOffScreen, newTop: mnp.lines, mnp: mnp];
lastLineOnScreen ←
FindLineBeforePlace[startLine: newBottomScreenLine, place: firstLineOffScreen];
IF lastLineOnScreen = NIL OR lastLineOnScreen.firstCharIndex # messageLength THEN
RefreshSoThatFirstCharStartsLine[firstChar: firstLineOffScreen.firstCharIndex,
firstLine: newBottomScreenLine, mnp: mnp]
ELSE ClearScreenLinesToTrailingBlankLines
[startLine: newBottomScreenLine, charIndex: messageLength, mnp: mnp]
END; -- of ScrollUpMessageNbr --


MakeCharIndexVisible: PUBLIC PROCEDURE [index: CharIndex,
mnp: MessageTextNbrPtr, offScreenSlop: CARDINAL ← 40] RETURNS [line: LinePtr] =
-- Ensures that the caret will be visible during type in.
BEGIN
message: vmD.VirtualMessagePtr ← mnp.message;
crCount: CARDINAL ← 0;
i, firstIndexOffScreen: CharIndex;
IF mnp.nLines < 2 THEN MoveDMCMBoundary
[intC.dmcmBoundaryPadNbr,
intC.dmcmBoundaryPadNbr.topY - dsD.lineHeight * (2 - mnp.nLines)];
IF (line ← MapCharIndexToLine[index, mnp]) # NIL THEN RETURN;
offScreenSlop ← 240; -- ##change in interface.
firstIndexOffScreen ← mnp.firstLineOffScreen.firstCharIndex;
IF index IN [firstIndexOffScreen .. firstIndexOffScreen + offScreenSlop]
AND mnp.lines.state # endOfMessage THEN
FOR i ← firstIndexOffScreen, i + 1 UNTIL i >= index DO
IF vmD.GetMessageChar[message, i] = Ascii.CR THEN crCount ← crCount + 1;
IF crCount > 4 THEN EXIT;
REPEAT
FINISHED => BEGIN
WHILE line = NIL DO
ScrollUpMessageNbr[mnp.topY + dsD.lineHeight, mnp];
line ← MapCharIndexToLine[index, mnp];
ENDLOOP;
RETURN;
END;
ENDLOOP;
RefreshToPlaceCharOnLine[index, (line ← mnp.lines), mnp];
END; -- of MakeCharIndexVisible --


MapYToNonBlankLine: PUBLIC PROCEDURE [y: ScreenYCoord, mnp: MessageTextNbrPtr]
RETURNS [line: LinePtr] =
-- Returns the screen line associated with y. If y falls on blank line below the end of
-- message, then returns the end of message line.
BEGIN
FOR line ← mnp.lines, line.nextLine
UNTIL (line.state = endOfMessage) OR (line.nextLine.y > y)
DO ENDLOOP;
END; -- of MapYToNonBlankLine --


ClearScreenLinesToTrailingBlankLines: PUBLIC PROCEDURE [startLine: LinePtr,
charIndex: CharIndex, mnp: MessageTextNbrPtr] =
-- Clears the screen bit map and fixes the line blocks for all lines from [startLine ..
-- firstLineOffScreen].
BEGIN
FOR line: LinePtr ← startLine, line.nextLine UNTIL line = NIL DO
IF line.state # trailingBlankLine AND line # mnp.firstLineOffScreen THEN
ClearLine[line];
line.firstCharIndex ← charIndex;
line.state ← trailingBlankLine;
line.rightX ← inD.leftMargin;
ENDLOOP;
END; -- of ClearScreenLinesToTrailingBlankLines --


ScrollDownMessageNbr: PUBLIC PROCEDURE [y: ScreenYCoord, mnp: MessageTextNbrPtr]=
-- y is the ScreenYCoord of the cursor. Scrolls the screen Down, based on this y position.
-- Adjusts all structures to reflect the new screen contents.
BEGIN
numberOfScreenLinesAbove, numberOfLinesAvailable, i: CARDINAL;
firstCharIndex: CharIndex ← mnp.lines.firstCharIndex;
line, oldBottomLine, newTopLine: LinePtr;
firstLineOffScreen: LinePtr = mnp.firstLineOffScreen;

numberOfScreenLinesAbove ← MAX[(y - mnp.lines.y) / dsD.lineHeight, 1];

-- The following block of code handles the possibility that there may not be enough text above the screen to be brought on, by adjusting newTopLine.
numberOfLinesAvailable ← 0;
FOR i IN [1 .. numberOfScreenLinesAbove] DO
IF firstCharIndex = 0 THEN EXIT;
firstCharIndex ← SearchBackForFirstCharIndexOfLine[firstCharIndex - 1, mnp.message];
numberOfLinesAvailable ← numberOfLinesAvailable + 1;
ENDLOOP;
newTopLine ← NthLineFrom
[mnp.lines, MIN[numberOfScreenLinesAbove, numberOfLinesAvailable]];

oldBottomLine ← mnp.lines;
FOR line ← newTopLine, line.nextLine UNTIL line = firstLineOffScreen DO
oldBottomLine ← oldBottomLine.nextLine ENDLOOP;

firstLineOffScreen.firstCharIndex ← oldBottomLine.firstCharIndex;
firstLineOffScreen.state ← oldBottomLine.state;
[] ← MoveLines[top: mnp.lines, bottom: oldBottomLine, newTop: newTopLine, mnp: mnp];

FOR line ← mnp.lines, line.nextLine UNTIL line = newTopLine DO
line.firstCharIndex ← firstCharIndex;
line.state ← normalText;
[firstCharIndex, , ] ← RompAndStomp[line, firstCharIndex, mnp];
ENDLOOP;
END; -- of ScrollDownNbr --


ThumbMessageNbr: PUBLIC PROCEDURE
[tlnp: ThumbLineNbrPtr, np: NbrPtr, x: ScreenXCoord] =
-- Displays that portion of the CM that begins at the same relative position in the CM as
-- x is in the thumb line.
BEGIN
xRange: CARDINAL ← tlnp.rightX - tlnp.leftX;
messageLength, newFirstCharIndex: CharIndex;
savedCursor: dsD.CursorShape;
mnp: MessageTextNbrPtr ←
WITH p: np SELECT FROM messageText => @p, ENDCASE => ERROR;

IF ~mnp.haveMessage OR (messageLength ← vmD.GetMessageSize[mnp.message]) = 0
THEN RETURN;
newFirstCharIndex ←
Inline.LongDiv[Inline.LongMult[messageLength, x - tlnp.leftX], xRange];
[savedCursor, , ] ← dsD.GetCursor[];
dsD.ChangeCursor[hourGlass];
RefreshToPlaceCharOnLine[newFirstCharIndex, mnp.lines, mnp];
dsD.ChangeCursor[savedCursor];
END; -- of ThumbMessageNbr --


Underline, DeUnderline: PUBLIC PROCEDURE
[start, end: CharIndex, underlineType: UnderlineType, pendingDelete: BOOLEAN,
mnp: MessageTextNbrPtr] =
-- Draws an underline indication on the screen for the visible section of the message in
-- the range [start .. end).
BEGIN
startLine, endLine, line: LinePtr;
startX, endX: ScreenXCoord;
firstCharOffScreen: CharIndex ← mnp.firstLineOffScreen.firstCharIndex;
firstCharOnScreen: CharIndex ← mnp.lines.firstCharIndex;
message: vmD.VirtualMessagePtr ← mnp.message;

IF (start >= firstCharOffScreen) OR (start >= end) THEN RETURN;
IF start < firstCharOnScreen THEN {start ← firstCharOnScreen; startLine ← mnp.lines}
ELSE startLine ← MapCharIndexToLine[start, mnp];

IF end <= firstCharOnScreen THEN RETURN;
IF end > firstCharOffScreen THEN
{end ← firstCharOffScreen; endLine ← mnp.firstLineOffScreen}
ELSE endLine ← MapCharIndexToLine[end - 1, mnp];

-- underline within [startline .. endline]
startX ← RangeRightXInLine[startLine.firstCharIndex, start, inD.leftMargin, message];
IF startLine = endLine THEN
BEGIN
endX ← RangeRightXInLine[start, end, startX, message];
DrawUnderlineInLine[startX, endX, startLine, underlineType, pendingDelete];
RETURN;
END;

-- underline right portion of first startLine.
DrawUnderlineInLine[startX, startLine.rightX, startLine, underlineType, pendingDelete];
-- underline entire lines between startLine and endLine.
FOR line ← startLine.nextLine, line.nextLine UNTIL line = endLine DO
DrawUnderlineInLine[inD.leftMargin, line.rightX, line, underlineType, pendingDelete];
ENDLOOP;
-- underline left portion of endLine.
endX ← RangeRightXInLine[endLine.firstCharIndex, end, inD.leftMargin, message];
DrawUnderlineInLine[inD.leftMargin, endX, endLine, underlineType, pendingDelete];
END; -- of Underline, DeUnderline --


DrawUnderlineInLine: PUBLIC PROCEDURE [start, end: ScreenXCoord, line: LinePtr,
underlineType: UnderlineType, pendingDelete: BOOLEAN] =
-- XOR’s an emphasis from start to end on line. If underlineType is pendingDelete, then
-- inverts entire lineheight, otherwise, underlines on the last scan line for line.
BEGIN
grayShade: dsD.GrayShade;
bottom: ScreenYCoord ← line.y + dsD.lineHeight;
top: ScreenYCoord ← bottom - 1;
SELECT underlineType FROM
target => {grayShade ← black; IF pendingDelete THEN top ← line.y};
source => {grayShade ← dottedLine; IF pendingDelete THEN top ← top - 1};
ENDCASE; -- on underLineType;
dsD.InvertRectangle[left: start, right: end, top: top, bottom: bottom, grayShade: grayShade];
IF pendingDelete AND underlineType = source THEN
dsD.InvertRectangle[left: start, right: end, top: line.y, bottom: top];
END; -- of DrawUnderlineInLine --


UnderlineSelection, DeUnderlineSelection: PUBLIC PROCEDURE
[selection: TextSelectionPtr, underline: UnderlineType] =
-- Removes the underline indication on the screen for the visible section of the message
-- in the range [start .. end).
BEGIN
Underline
[selection.start, selection.end, underline, selection.pendingDelete, selection.mnp];
END; -- of UnderlineSelection, DeUnderlineSelection --


InitializeSelection: PUBLIC PROCEDURE [selection: TextSelectionPtr] =
-- Initializes a selection to its default values.
BEGIN
selection↑ ← TextSelection[intC.cmTextNbr, 0, 0, 0, 0, char, FALSE];
END; -- of InitializeSelection --


ClearSourceSelection: PUBLIC PROCEDURE [unlock: BOOLEAN ← TRUE] =
-- DeUnderlines and reinitializes intC.source.
BEGIN
sourceSel: TextSelectionPtr = @intC.source;
IF (sourceSel.key = 0) # sourceSel.mnp.editable THEN exD.SysBug[];
DeUnderlineSelection[sourceSel, source];
IF unlock AND sourceSel.key # 0 THEN
vmD.UnlockTOC[vmD.DisplayMessage[sourceSel.mnp.message].toc, sourceSel.key];
InitializeSelection[@intC.source];
END; -- of ClearSourceSelection --


MoveLines: PUBLIC PROCEDURE [top, bottom, newTop: LinePtr, mnp: MessageTextNbrPtr]
RETURNS [newBottom: LinePtr] =
-- Moves information on screen and updates line structures appropriately.
BEGIN
-- Move lines from [top .. bottom) to [newTop .. newBottom). Clear screen for each target line before moving.
line: LinePtr;

newBottom ← newTop;
FOR line ← top, line.nextLine UNTIL line = bottom DO
newBottom ← newBottom.nextLine;
ENDLOOP;
dsD.SlideFullWidthRectangleVertically[top.y, bottom.y, newTop.y];
IF top.y >= newTop.y THEN MoveLineDescriptors[top, bottom, newTop]
ELSE BEGIN
-- lines are moving down, reverse chain destructively, move line descriptors between
-- proper endpoints, and reverse chain destructively to restore chain.
DestructiveReverse[mnp.lines];
MoveLineDescriptors
[top: bottom.nextLine, bottom: top.nextLine, newTop: newBottom.nextLine];
DestructiveReverse[mnp.firstLineOffScreen];
END;
END; -- of MoveLines --


MoveLineDescriptors: PROCEDURE [top, bottom, newTop: LinePtr] =
-- Updates line structures so that information previously contained in [top .. bottom) is
-- moved to [newTop .. <newBottom>). newTop is guaranteed higher in line chain than
-- top upon entry.
BEGIN
line, newLine, saveNextLine: LinePtr;
saveY: ScreenYCoord;

newLine ← newTop;
FOR line ← top, line.nextLine UNTIL line = bottom DO
saveY ← newLine.y;
saveNextLine ← newLine.nextLine;
newLine↑ ← line↑;
newLine.y ← saveY;
newLine ← newLine.nextLine ← saveNextLine;
ENDLOOP;
END; -- of MoveLineDescriptors --


SearchBackForFirstCharIndexOfLine: PROCEDURE [startIndex: CharIndex,
message: vmD.VirtualMessagePtr] RETURNS [firstChar: CharIndex] =
-- Returns the largest charIndex less than or equal to index that imediately follows a CR
-- or PCR.
BEGIN
formatGap: CARDINAL = 1000;
i, formatStartIndex: CharIndex;
char: CHARACTER;
get: POINTER TO vmD.CharCache ← @message.get;
FOR i DECREASING IN [0 .. startIndex) DO
char ← IF i IN [get.first .. get.free)
THEN message.buffer.chars[i + get.floor - get.first]
ELSE vmD.GetMessageChar[message, i];
IF (char = Ascii.CR)
OR (char >= ovD.LineBreakValue AND i IN [message.formatStart .. message.formatEnd))
THEN {firstChar ← i + 1; EXIT};
REPEAT
FINISHED => firstChar ← 0;
ENDLOOP;
IF startIndex IN [message.formatStart .. message.formatEnd)
AND firstChar IN [message.formatStart .. message.formatEnd) THEN RETURN;
-- Format lines up through startIndex.
-- Choose starting character index for formatting.
SELECT TRUE FROM
(firstChar >= message.formatEnd AND firstChar - message.formatEnd < formatGap)
=> formatStartIndex ← message.formatEnd;
(firstChar < message.formatStart AND firstChar < formatGap)
=> message.formatStart ← formatStartIndex ← 0;
ENDCASE => message.formatStart ← formatStartIndex ← firstChar;
DO
[formatStartIndex, , ] ← FormatLine[formatStartIndex, message];
IF formatStartIndex >= startIndex THEN EXIT;
firstChar ← formatStartIndex;
ENDLOOP;
message.formatEnd ← formatStartIndex;
END; -- of SearchBackForFirstCharIndexOfLine --


RompAndStomp: PROCEDURE [line: LinePtr, firstChar: CharIndex,
mnp: MessageTextNbrPtr, underline: BOOLEAN ← TRUE]
RETURNS [firstUnusedCharIndex: CharIndex, curRightX: ScreenXCoord,
curY: ScreenYCoord] =
-- Previously called DisplayOnePreviouslyFormattedLine. Selections will be refreshed with
-- their underlines iff underline is TRUE.
BEGIN
message: vmD.VirtualMessagePtr ← mnp.message;
messageLength: CharIndex ← vmD.GetMessageSize[message];

PaintUnderline: PROCEDURE [selection: POINTER TO TextSelection,
underlineType: UnderlineType] =
-- Paints underlines for RompAndStomp.
BEGIN
iStart, iEnd: CharIndex;
startX, endX: ScreenXCoord;
iStart ← IF selection.start > firstChar THEN selection.start ELSE firstChar;
iEnd ← IF selection.end < firstUnusedCharIndex THEN selection.end
ELSE firstUnusedCharIndex;
IF iStart >= iEnd THEN iStart ← iEnd ← 0;
IF (iStart = firstChar) AND (iEnd = firstUnusedCharIndex) THEN
DrawUnderlineInLine
[leftMargin, curRightX, line, underlineType, selection.pendingDelete]
ELSE IF iStart # iEnd THEN
BEGIN
startX ← RangeRightXInLine[firstChar, iStart, inD.leftMargin, message];
endX ← RangeRightXInLine[iStart, iEnd, startX, message];
DrawUnderlineInLine[startX, endX, line, underlineType, selection.pendingDelete];
END;
END; -- of PaintUnderline --

curY ← line.y;
line.firstCharIndex ← firstChar;
line.state ← normalText;
[curRightX, firstUnusedCharIndex] ← CharacterBlaster
[firstChar, messageLength, message, inD.leftMargin, curY];
line.rightX ← curRightX;
IF underline THEN
BEGIN
IF intC.target.mnp = mnp THEN PaintUnderline[@intC.target, target];
IF intC.source.mnp = mnp THEN PaintUnderline[@intC.source, source];
END;
END; -- of RompAndStomp --


FormatMessage: PUBLIC PROCEDURE [vm: vmD.VirtualMessagePtr] =
-- Formats all of the composed message from the high water mark to the end,
-- expanding the message to break up long lines if necessary. cm.formatEnd is
-- set to the end of the message.
BEGIN
index: CharIndex ← IF vm.formatStart = 0 THEN vm.formatEnd ELSE 0;
messageLength: CharIndex ← vmD.GetMessageSize[vm];
vm.formatStart ← 0;
UNTIL index >= messageLength DO
[index, , ] ← FormatLine[index, vm];
ENDLOOP;
vm.formatEnd ← messageLength;
END; -- of FormatMessage --


FormatAndDisplaySuffix: PROCEDURE [mnp: MessageTextNbrPtr, firstChar,
firstCharOfLine: CharIndex, firstX: ScreenXCoord, line: LinePtr]
RETURNS [firstUnusedCharIndex: CharIndex, curX: ScreenXCoord] =
-- Formats (fixes PCR’s) of the suffix of one line
BEGIN
IF firstChar = vmD.GetMessageSize[mnp.message] THEN
{firstUnusedCharIndex ← firstChar; curX ← firstX}
ELSE BEGIN
[firstUnusedCharIndex, curX, ] ← FormatLine[firstCharOfLine, mnp.message];
IF curX < firstX THEN dsD.ClearRectangle[curX, firstX, line.y, line.nextLine.y]
ELSE
[ , ] ← CharacterBlaster[firstChar, firstUnusedCharIndex, mnp.message, firstX, line.y];
END;
END; -- of FormatAndDisplaySuffix --


FormatLine: PROCEDURE [firstCharOfLine: CharIndex,
message: vmD.VirtualMessagePtr]
RETURNS [breakIndex: CharIndex, breakX: ScreenXCoord, wasOk: BOOLEAN] =
-- Formats the suffix of a line by finding how much will fit on the remainder of the line
-- and changing the last blank to a PCR. Returns wasOk = TRUE iff the last blank that
-- got turned into a PCR actually was a PCR before FormatSuffix was called.
BEGIN
firstFreeIndex, index: CharIndex;
lastPCR: CharIndex ← 0;
rightX: ScreenXCoord ← inD.leftMargin;
i: CARDINAL ← 0;
char, prevChar, pcrChar: CHARACTER ← 0C;
get: POINTER TO vmD.CharCache ← @message.get;
prevCharBreakProp, charBreakProp, lineBreakProp: dsD.CharProperty ← alphaNumeric;

InlineGetCharRightX: PROC [char: CHARACTER, leftX: ScreenXCoord]
RETURNS [rightX: ScreenXCoord] = INLINE
-- Given a char positioned at leftX, returns its rightX. Char is
-- always masked to 7 bits. Tab widths may vary depending on
-- leftX.
BEGIN
offsetPtr: POINTER;
tabWidth: CARDINAL = 40;
IF char = Ascii.TAB THEN
RETURN[(((leftX + 5 - dsD.xOrigin) / tabWidth) + 1) * tabWidth + dsD.xOrigin];
offsetPtr ← disC.mcFont + LOOPHOLE[char, CARDINAL];
RETURN[leftX + LOOPHOLE[offsetPtr + offsetPtr↑, dsD.CharFontPtr].width];
END; -- of InlineGetCharRightX --


firstFreeIndex ← vmD.GetMessageSize[message];
FOR index ← firstCharOfLine, index + 1 UNTIL index >= firstFreeIndex DO
char ← IF index IN [get.first .. get.free)
THEN message.buffer.chars[index + get.floor - get.first]
ELSE vmD.GetMessageChar[message, index];
IF char >= ovD.LineBreakValue THEN
BEGIN
vmD.PutMessageChar[message, index, (char ← Inline.BITAND[char, ovD.CharMask])];
lastPCR ← index;
END;
IF char = Ascii.CR THEN RETURN[index + 1, rightX + inD.CRWidth, TRUE];
charBreakProp ← disC.charPropertyTable[char];
IF lineBreakProp = white THEN
{IF prevCharBreakProp = white AND charBreakProp # white THEN
{breakIndex ← index; breakX ← rightX; pcrChar ← prevChar}}
ELSE BEGIN
IF lineBreakProp = alphaNumeric OR prevCharBreakProp # alphaNumeric THEN
BEGIN
breakIndex ← index; breakX ← rightX; pcrChar ← prevChar;
IF prevCharBreakProp # white OR charBreakProp # white THEN
lineBreakProp ← prevCharBreakProp;
END;
END;
IF (rightX ← InlineGetCharRightX[char, rightX]) > inD.rightMargin - inD.CRWidth THEN
BEGIN
IF rightX > inD.rightMargin OR charBreakProp # white
OR (index + 1 < firstFreeIndex
AND dsD.GetCharBreakProp[vmD.GetMessageChar[message, index + 1]] = white)
THEN BEGIN
vmD.PutMessageChar
[message, breakIndex - 1, Inline.BITOR[pcrChar, ovD.LineBreakValue]];
RETURN[breakIndex, breakX, ((lastPCR + 1) = breakIndex)];
END
ELSE
BEGIN
vmD.PutMessageChar[message, index, Inline.BITOR[char, ovD.LineBreakValue]];
RETURN[index + 1, rightX, (lastPCR = index)];
END;
END;
prevCharBreakProp ← charBreakProp;
prevChar ← char;
ENDLOOP;
RETURN[index, rightX, TRUE];
END; -- of FormatLine --


FindLineBeforePlace: PROCEDURE [startLine, place: LinePtr] RETURNS [line: LinePtr] =
-- Returns the line preceeding place in the linked line list. Search begins at startLine. Returns NIL if startLine = place or if startLine occurs after place in list.
BEGIN
FOR line ← startLine, line.nextLine UNTIL (line = NIL) OR (line.nextLine = place)
DO ENDLOOP;
END; -- of FindLineBeforePlace --


MapCharIndexToLine: PUBLIC PROCEDURE [index: CharIndex,
mnp: MessageTextNbrPtr] RETURNS [LinePtr] =
-- Returns the screen line that contains index. Returns NIL if index is not currently
-- displayed on any visible screen line.
BEGIN
-- Assume that lines are maintained so that the firstCharIndex = MessageSize for any line
-- after the end of the message. Also assume that a trailing line (firstLineOffScreen) is
-- maintained.
line, nextLine: LinePtr;
char: CHARACTER;
prevIndexIsLineBreak: BOOLEAN
← (index = 0 OR (char ← vmD.GetMessageChar[mnp.message, index - 1]) = Ascii.CR
OR char >= ovD.LineBreakValue);

IF index ~ IN [mnp.lines.firstCharIndex .. mnp.firstLineOffScreen.firstCharIndex]
THEN RETURN[NIL];
FOR line ← mnp.lines, nextLine UNTIL line = mnp.firstLineOffScreen DO
nextLine ← line.nextLine;
IF nextLine.firstCharIndex > index
OR (line.state = endOfMessage AND prevIndexIsLineBreak)
OR (nextLine.state = endOfMessage AND ~prevIndexIsLineBreak)
THEN RETURN[line];
ENDLOOP;
RETURN[NIL];
END; -- of MapCharIndexToLine --


RangeRightXInLine: PUBLIC PROCEDURE [start, to: CharIndex, startX: ScreenXCoord,
message: vmD.VirtualMessagePtr] RETURNS [rightX: ScreenXCoord] =
-- Returns the sum of the widths in screen raster points of the characters from the
-- interval [start .. to), starting at X Coordinate startX in messsage.
BEGIN
i: CharIndex;
get: POINTER TO vmD.CharCache ← @message.get;
rightX ← startX;
FOR i IN [start .. to) DO
rightX ← dsD.GetCharRightX[IF i IN [get.first .. get.free)
THEN message.buffer.chars[i + get.floor - get.first]
ELSE vmD.GetMessageChar[message, i], rightX]
ENDLOOP
END; -- of RangeRightXInLine --


MapCharIndexInLineToLeftX: PUBLIC PROCEDURE [index: CharIndex, line: LinePtr,
message: vmD.VirtualMessagePtr] RETURNS [x: ScreenXCoord] =
-- index is in line.
-- Returns the ScreenXCoord of the left edge of the character corresponding to index in
-- message.
BEGIN
RETURN[RangeRightXInLine
[start: line.firstCharIndex, to: index, startX: inD.leftMargin, message: message]];
END; -- of MapCharIndexInLineToLeftX --


AdjustFirstCharIndices: PUBLIC PROCEDURE [firstLine: LinePtr, increment: INTEGER] =
-- Increments the firstCharIndex of all screen lines from firstLine on.
BEGIN
line: LinePtr;
FOR line ← firstLine, line.nextLine UNTIL line = NIL DO
line.firstCharIndex ← line.firstCharIndex + increment;
ENDLOOP;
END; -- of AdjustFirstCharIndices --


MapCursorToCharIndex: PUBLIC PROCEDURE [x: ScreenXCoord, y: ScreenYCoord,
mnp: MessageTextNbrPtr] RETURNS [index: CharIndex] =
-- Map x,y to a char index in message. If x,y is to the right of the rightmost char on its
-- line, treat as rightmost character. Similarly, if y is too far down on screen, treat as if
-- y was on the last screen line that holds normal message text.
BEGIN
line: LinePtr;
messageLength: CharIndex = vmD.GetMessageSize[mnp.message];
FOR line ← mnp.lines, line.nextLine
UNTIL (line.nextLine.firstCharIndex = messageLength) OR (line.nextLine.y > y)
DO ENDLOOP;
[index, , ] ← MapXInLineToCharIndex[x, line, mnp.message]
END; -- of MapCursorToCharIndex --


NextUnit: PROCEDURE [message: vmD.VirtualMessagePtr, startIndex: CharIndex,
startX: ScreenXCoord]
RETURNS[rightX: CARDINAL, endIndex: CharIndex] =
-- Computes rightX of run of characters plus run of following white space [startIndex ..
-- endIndex) in message if placed starting at startX.
BEGIN
state: {black, white} ← black;
char: CHARACTER;
rightX ← startX;

FOR endIndex IN [startIndex .. vmD.GetMessageSize[message]) DO
char ← vmD.GetMessageChar[message, endIndex];
IF char = Ascii.CR THEN RETURN[rightX + inD.CRWidth, endIndex + 1];
IF dsD.GetCharProperty[char, white] THEN state ← white
ELSE IF state = white THEN RETURN;
rightX ← dsD.GetCharRightX[char, rightX];
ENDLOOP;
endIndex ← vmD.GetMessageSize[message];
END; -- of NextUnit --


NthLineFrom: PUBLIC PROCEDURE [line: LinePtr, n: CARDINAL]
RETURNS [nthLine: LinePtr] =
-- Returns the nth line from line in the line chain, or NIL if fewer than n lines remain.
BEGIN
nthLine ← line;
THROUGH [1 .. n] UNTIL nthLine = NIL DO
nthLine ← nthLine.nextLine;
ENDLOOP;
END; -- of NthLineFrom --


ClearLine: PROCEDURE [line: LinePtr] =
{dsD.ClearRectangle[inD.leftMargin, line.rightX, line.y, line.nextLine.y]};


DestructiveReverse: PROCEDURE [line: LinePtr] =
-- Destructively reverse all pointers in line chain starting with line.
BEGIN
next, prior: LinePtr;
prior ← NIL;
UNTIL line = NIL DO
next ← line.nextLine;
line.nextLine ← prior;
prior ← line;
line ← next;
ENDLOOP;
END; -- of DestructiveReverse --


Even: PROCEDURE [nWords: CARDINAL] RETURNS [POINTER] =
-- allocates n words starting at an even address.
BEGIN
p: POINTER ← Storage.Node[nWords+1];
RETURN[p + Inline.BITAND[p, 1]];
END; -- of Even --


--Initialization of the BITBLT Table for CharacterBlaster
IF ~FrameDefs.GlobalFrame[dsD.PaintPicture].started THEN
FrameOps.Start[LOOPHOLE[FrameDefs.GlobalFrame[dsD.PaintPicture]]];
charBbtPtr ← dsD.charBbtPtr;


END. -- of EditorDisplay --