/******************************************************************************
 JTextEditor.cc

	Class to edit styled text.

	Only public and protected functions are required to call NewUndo(),
	and only if the action to be performed changes the text or styles.
	Private functions must -not- call NewUndo(), because several manipulations
	may be required to perform one user command, and only the user command
	as a whole is undoable.  (Otherwise, the user may get confused.)

	Because of this convention, public functions that affect undo should only
	be a public interface to a private function.  The public function calls
	NewUndo() and then calls the private function.  The private function
	does all the work, but doesn't modify the undo information.  This allows other
	private functions to use the routine (private version) without modifying the
	undo information.

	In order to guarantee that the TextChanged message means "text has already
	changed", NewUndo() must be called -after- making the modification.
	(even though the Undo object has to be created before the modifications)

	TextSet is different from TextChanged because documents will typically
	use only the latter for setting their "unsaved" flag.

	When the text ends with a newline, we have to draw the caret on the
	next line.  This is a special case because (charIndex == bufLength+1)
	would normally catch this.  The code required to handle this special
	case is marked with "ends with newline".  (Most, but not all, such
	code calls EndsWithNewline().)

	Derived classes must implement the following routines:

		TERefresh
			Put an update event in the queue to redraw the text.

		TERefreshRect
			Put an update event in the queue to redraw part of the text.

		TEUpdateDisplay
			Redraw whatever was requested by TERefresh() and TERefreshRect().

		TERedraw
			Redraw the text immediately.

		TESetGUIBounds
			Set the bounds of the GUI object to match our size.  The original
			height and vertical position of the change can be used to update
			scrolltabs.  If change position is negative, don't update the
			scrolltabs.

		TEScrollToRect
			Scroll the text to make the given rectangle visible, possibly
			centering it in the middle of the display.  Return kTrue
			if scrolling was necessary.

		TEScrollForDrag
			Scroll the text to make the given point visible.  Return kTrue
			if scrolling was necessary.

		TESetVertScrollStep
			Set the vertical step size and page context used when scrolling
			the text.

		TECaretShouldBlink
			If blink is kTrue, reset the timer and make the caret blink by
			calling TEShow/HideCaret().  Otherwise, call TEHideCaret().
			The derived class' constructor must call TECaretShouldBlink(kTrue)
			because it is pure virtual for us.

		TEClipboardChanged
			Do whatever is appropriate to update the system clipboard
			after a Copy or Cut operation.

		TEGetExternalClipboard
			Returns kTrue if there is something pasteable on the system clipboard.

		TEBeginDND
			Returns kTrue if it is able to start a Drag-And-Drop session
			with the OS.  From then on, the derived class should call the
			TEHandleDND*() functions.  It must also call TEDNDFinished()
			when the drag ends.  If TEBeginDND() returns kFalse, then we
			will manage an internal DND session.  In this case, the derived
			class should continue to call the usual TEHandleMouse*()
			functions.

		TEPasteDropData
			Get the data that was dropped and use Paste(text,style)
			to insert it.  (Before this is called, the insertion point
			is set so that Paste() will work correctly.)

	To draw page headers and footers while printing, override the
	following routines:

		GetPrintHeaderHeight
			Return the height required for the page header.

		DrawPrintHeader
			Draw the page header.  JTable will lock the header afterwards.

		GetPrintFooterHeight
			Return the height required for the page footer.

		DrawPrintFooter
			Draw the page footer.  JTable will lock the footer afterwards.

	The default implementation of tabs rounds the location up to the nearest
	multiple of itsDefTabWidth.  To implement non-uniform tabs or tabs on a
	line-by-line basis, override the following function:

		GetTabWidth
			Given the index of the tab character (charIndex) and the horizontal
			position on the line (in pixels) where the tab character starts (x),
			return the width of the tab character.

	Functionality yet to be implemented:

		Search & replace for text and styles

	BASE CLASS = virtual JBroadcaster

	Copyright © 1996-99 by John Lindal. All rights reserved.

 ******************************************************************************/

#include <JTextEditor.h>
#include <JTEUndoTyping.h>
#include <JTEUndoPaste.h>
#include <JTEUndoDrop.h>
#include <JTEUndoStyle.h>
#include <JTEUndoTabShift.h>
#include <JPagePrinter.h>
#include <JFontManager.h>
#include <JColormap.h>
#include <JOrderedSetUtil.h>
#include <JRunArrayIterator.h>
#include <JRegex.h>
#include <JLatentPG.h>
#include <JMinMax.h>
#include <jASCIIConstants.h>
#include <jFStreamUtil.h>
#include <jStreamUtil.h>
#include <jStrStreamUtil.h>
#include <jTime.h>
#include <ctype.h>
#include <jGlobals.h>
#include <jAssert.h>

const JCoordinate kDefLeftMarginWidth = 10;
const JCoordinate kRightMarginWidth   = 2;

const JCoordinate kDebounceWidth        = 3;
const JCoordinate kDraggedOutlineRadius = 10;

const JFileVersion kCurrentPrivateFormatVersion = 1;

const JSize kDefaultMaxUndoCount = 100;

const JSize kUNIXLineWidth    = 75;
const JSize kUNIXTabCharCount = 8;

static const JCharacter* kCRMCaretActionText     = "Clean paragraph margins";
static const JCharacter* kCRMSelectionActionText = "Clean margins for selection";

static const JCharacter* kCRM2CaretActionText     = "Coerce paragraph margins";
static const JCharacter* kCRM2SelectionActionText = "Coerce margins for selection";

JBoolean JTextEditor::itsCopyWhenSelectFlag = kFalse;

// JBroadcaster message types

const JCharacter* JTextEditor::kTypeChanged      = "TypeChanged::JTextEditor";
const JCharacter* JTextEditor::kTextSet          = "TextSet::JTextEditor";
const JCharacter* JTextEditor::kTextChanged      = "TextChanged::JTextEditor";
const JCharacter* JTextEditor::kCaretLineChanged = "CaretLineChanged::JTextEditor";

/******************************************************************************
 Constructor

	We don't provide a constructor that accepts text because we
	can't call RecalcAll() due to pure virtual functions.

	*** Derived classes must call RecalcAll().

 ******************************************************************************/

JTextEditor::JTextEditor
	(
	const Type			type,
	const JBoolean		breakCROnly,
	const JBoolean		pasteStyledText,
	const JFontManager*	fontManager,
	JColormap*			colormap,
	const JColorIndex	caretColor,
	const JColorIndex	selectionColor,
	const JColorIndex	outlineColor,
	const JColorIndex	dragColor,
	const JCoordinate	width
	)
	:
	JBroadcaster(),

	itsType(type),
	itsPasteStyledTextFlag(pasteStyledText),

	itsFontMgr(fontManager),
	itsColormap(colormap),

	itsCaretColor(caretColor),
	itsSelectionColor(selectionColor),
	itsSelectionOutlineColor(outlineColor),
	itsDragColor(dragColor)
{
	itsBuffer = new JString;
	assert( itsBuffer != NULL );

	itsStyles = new JRunArray<Font>;
	assert( itsStyles != NULL );

	itsCustomColors          = NULL;
	itsUndo                  = NULL;
	itsUndoList              = NULL;
	itsFirstRedoIndex        = 1;
	itsUndoState             = kIdle;
	itsMaxUndoCount          = kDefaultMaxUndoCount;
	itsActiveFlag            = kFalse;
	itsSelActiveFlag         = kFalse;
	itsCaretVisibleFlag      = kFalse;
	itsPerformDNDFlag        = kFalse;
	itsAutoIndentFlag        = kFalse;
	itsMoveToFrontOfTextFlag = kFalse;
	itsBreakCROnlyFlag       = breakCROnly;
	itsIsPrintingFlag        = kFalse;

	itsDefFont.size = kJDefaultFontSize;
	// itsDefFont.style is automatically empty
	itsDefFont.id = itsFontMgr->GetFontID(JGetDefaultFontName(), itsDefFont.size, itsDefFont.style);

	itsWidth           = width - kDefLeftMarginWidth - kRightMarginWidth;
	itsHeight          = 0;
	itsGUIWidth        = itsWidth;
	itsLeftMarginWidth = kDefLeftMarginWidth;
	itsDefTabWidth     = 36;	// 1/2 inch
	itsMaxWordWidth    = 0;

	itsLineStarts = new JArray<JIndex>;
	assert( itsLineStarts != NULL );
	itsLineStarts->SetCompareFunction(JCompareIndices);
	itsLineStarts->SetSortOrder(JOrderedSetT::kSortAscending);

	itsLineGeom = new JRunArray<LineGeometry>;
	assert( itsLineGeom != NULL );

	itsCaretLoc      = CaretLocation(1,1);
	itsCaretX        = 0;
	itsInsertionFont = CalcInsertionFont(1);

	itsClipText  = NULL;
	itsClipStyle = NULL;

	itsDragText  = NULL;
	itsDragStyle = NULL;

	itsPrevDragType = itsDragType = kInvalidDrag;
	itsIsDragSourceFlag = kFalse;

	itsPrevBufLength    = 0;
	itsCRMLineWidth     = kUNIXLineWidth;
	itsCRMTabCharCount  = kUNIXTabCharCount;
	itsCRMRuleList      = NULL;
	itsOwnsCRMRulesFlag = kFalse;

	itsHTMLLexerState = NULL;

	if (type == kFullEditor)
		{
		itsBuffer->SetBlockSize(1024);
		itsLineStarts->SetBlockSize(100);
		}
}

/******************************************************************************
 Copy constructor

 ******************************************************************************/

JTextEditor::JTextEditor
	(
	const JTextEditor& source
	)
	:
	JBroadcaster(source),

	itsType( source.itsType ),
	itsPasteStyledTextFlag( source.itsPasteStyledTextFlag ),

	itsFontMgr( source.itsFontMgr ),
	itsColormap( source.itsColormap ),

	itsCaretColor( source.itsCaretColor ),
	itsSelectionColor( source.itsSelectionColor ),
	itsSelectionOutlineColor( source.itsSelectionOutlineColor ),
	itsDragColor( source.itsDragColor )
{
	itsBuffer = new JString(*(source.itsBuffer));
	assert( itsBuffer != NULL );

	itsStyles = new JRunArray<Font>(*(source.itsStyles));
	assert( itsStyles != NULL );

	itsCustomColors = NULL;
	if (source.itsCustomColors != NULL)
		{
		itsCustomColors = new JArray<JColorIndex>(*(source.itsCustomColors));
		assert( itsCustomColors != NULL );
		itsCustomColors->SetCompareFunction(JCompareIndices);
		const JSize colorCount = itsCustomColors->GetElementCount();
		for (JIndex i=1; i<colorCount; i++)
			{
			itsColormap->UsingColor(itsCustomColors->GetElement(i));
			}
		}

	itsUndo                  = NULL;
	itsUndoList              = NULL;
	itsFirstRedoIndex        = 1;
	itsUndoState             = kIdle;
	itsMaxUndoCount          = source.itsMaxUndoCount;
	itsActiveFlag            = kFalse;
	itsSelActiveFlag         = kFalse;
	itsCaretVisibleFlag      = kFalse;
	itsPerformDNDFlag        = source.itsPerformDNDFlag;
	itsAutoIndentFlag        = source.itsAutoIndentFlag;
	itsMoveToFrontOfTextFlag = source.itsMoveToFrontOfTextFlag;
	itsBreakCROnlyFlag       = source.itsBreakCROnlyFlag;
	itsIsPrintingFlag        = kFalse;

	itsDefFont         = source.itsDefFont;
	itsWidth           = source.itsWidth;
	itsHeight          = source.itsHeight;
	itsGUIWidth        = source.itsGUIWidth;
	itsLeftMarginWidth = source.itsLeftMarginWidth;
	itsDefTabWidth     = source.itsDefTabWidth;
	itsMaxWordWidth    = source.itsMaxWordWidth;

	itsLineStarts = new JArray<JIndex>(*(source.itsLineStarts));
	assert( itsLineStarts != NULL );

	itsLineGeom = new JRunArray<LineGeometry>(*(source.itsLineGeom));
	assert( itsLineGeom != NULL );

	itsPrevBufLength = source.itsPrevBufLength;

	itsCaretLoc      = CaretLocation(1,1);
	itsCaretX        = 0;
	itsInsertionFont = CalcInsertionFont(1);

	itsClipText         = NULL;
	itsClipStyle        = NULL;

	itsDragText         = NULL;
	itsDragStyle        = NULL;

	itsPrevDragType     = itsDragType = kInvalidDrag;
	itsIsDragSourceFlag = kFalse;

	itsCRMLineWidth     = source.itsCRMLineWidth;
	itsCRMTabCharCount  = source.itsCRMTabCharCount;

	itsCRMRuleList      = NULL;
	itsOwnsCRMRulesFlag = kFalse;
	if (source.itsCRMRuleList != NULL)
		{
		SetCRMRuleList(source.itsCRMRuleList, source.itsOwnsCRMRulesFlag);
		}

	itsHTMLLexerState = NULL;
}

/******************************************************************************
 Destructor

 ******************************************************************************/

JTextEditor::~JTextEditor()
{
	delete itsBuffer;
	delete itsStyles;
	delete itsUndo;
	delete itsLineStarts;
	delete itsLineGeom;

	if (itsUndoList != NULL)
		{
		itsUndoList->DeleteAll();
		delete itsUndoList;
		}

	delete itsClipText;
	delete itsClipStyle;

	delete itsDragText;
	delete itsDragStyle;

	ClearCRMRuleList();

	if (itsCustomColors != NULL)
		{
		const JSize count = itsCustomColors->GetElementCount();
		for (JIndex i=1; i<=count; i++)
			{
			itsColormap->DeallocateColor(itsCustomColors->GetElement(i));
			}
		delete itsCustomColors;
		}
}

/******************************************************************************
 SetType (protected)

	This is protected because most derived classes will not be written
	to expect the type to change.

	Broadcasts TypeChanged.

 ******************************************************************************/

void
JTextEditor::SetType
	(
	const Type type
	)
{
	itsType = type;
	TERefresh();
	Broadcast(TypeChanged(type));
}

/******************************************************************************
 SetText

	Returns kFalse if illegal characters had to be removed.

	This is not accessible to the user, so we don't provide Undo.

 ******************************************************************************/

JBoolean
JTextEditor::SetText
	(
	const JCharacter*		text,
	const JRunArray<Font>*	style
	)
{
	*itsBuffer = text;
	return SetText1(style);
}

/******************************************************************************
 SetText1 (private)

	Returns kFalse if illegal characters had to be removed.

	style can be NULL or itsStyles.

 ******************************************************************************/

JBoolean
JTextEditor::SetText1
	(
	const JRunArray<Font>* style
	)
{
	ClearUndo();

	JBoolean cleaned = kFalse;

	if (style != NULL)
		{
		assert( itsBuffer->GetLength() == style->GetElementCount() );
		*itsStyles = *style;
		cleaned = RemoveIllegalChars(itsBuffer, itsStyles);
		}
	else
		{
		cleaned = RemoveIllegalChars(itsBuffer);

		itsStyles->RemoveAll();
		if (!itsBuffer->IsEmpty())
			{
			itsStyles->AppendElements(itsDefFont, itsBuffer->GetLength());
			}
		}

	SetCaretLocation(1);
	RecalcAll(kTrue);
	Broadcast(TextSet());
	return JNegate(cleaned);
}

/******************************************************************************
 ReadPlainText

	Returns kFalse if illegal characters had to be removed.

	We don't call SetText to avoid making two copies of the file's data.
	(The file could be very large.)

 ******************************************************************************/

#define CONVERT_DOS_MEMORY	0
#define CONVERT_DOS_FILE	1

static const JCharacter* kUNIXNewline          = "\n";
static const JCharacter  kUNIXNewlineChar      = '\n';
static const JCharacter* kMacintoshNewline     = "\r";
static const JCharacter  kMacintoshNewlineChar = '\r';
static const JCharacter* kDOSNewline           = "\r\n";
static const JCharacter  k1stDOSNewlineChar    = '\r';
static const JCharacter  k2ndDOSNewlineChar    = '\n';

JBoolean
JTextEditor::ReadPlainText
	(
	const JCharacter*	fileName,
	PlainTextFormat*	format
	)
{
	TEDisplayBusyCursor();

	JReadFile(fileName, itsBuffer);

	JIndex i;
	if (ContainsIllegalChars(*itsBuffer))
		{
		// It is probably a binary file, so we shouldn't mess with it.

		*format = kUNIXText;
		}

	else if (itsBuffer->LocateLastSubstring(kMacintoshNewline, &i))
		{
		// We work from the end since DOS strings will shrink.

		if (i < itsBuffer->GetLength() &&
			itsBuffer->GetCharacter(i+1) == k2ndDOSNewlineChar)
			{
			*format = kDOSText;

			const JSize origLength = itsBuffer->GetLength();
			JLatentPG pg(100);
			pg.FixedLengthProcessBeginning(origLength,
					"Converting from DOS format...", kFalse, kFalse);

			#if CONVERT_DOS_MEMORY

			do		// this is more than 100 times slower!
				{
				itsBuffer->ReplaceSubstring(i,i+1, kUNIXNewline);
				pg.IncrementProgress(origLength - i - pg.GetCurrentStepCount());
				}
				while (itsBuffer->LocatePrevSubstring(kDOSNewline, &i));

			#elif CONVERT_DOS_FILE

			const JSize saveSize = itsBuffer->GetBlockSize();
			itsBuffer->SetBlockSize(origLength);

			itsBuffer->Clear();
			ifstream input(fileName);
			while (1)
				{
				const JCharacter c = input.get();
				if (input.eof() || input.fail())
					{
					break;
					}

				if (c != k1stDOSNewlineChar)
					{
					itsBuffer->AppendCharacter(c);
					}
				else
					{
					pg.IncrementProgress(itsBuffer->GetLength() - pg.GetCurrentStepCount());
					}
				}
			input.close();

			itsBuffer->SetBlockSize(saveSize);

			#endif

			pg.ProcessFinished();
			}

		else
			{
			*format = kMacintoshText;

			const JCharacter* s = itsBuffer->GetCString();
			while (i > 0)
				{
				i--;
				if (s[i] == kMacintoshNewlineChar)
					{
					itsBuffer->SetCharacter(i+1, kUNIXNewlineChar);
					}
				}
			}
		}

	else
		{
		*format = kUNIXText;
		}

	return SetText1(NULL);
}

/******************************************************************************
 WritePlainText

 ******************************************************************************/

void
JTextEditor::WritePlainText
	(
	const JCharacter*		fileName,
	const PlainTextFormat	format
	)
	const
{
	ofstream output(fileName);
	WritePlainText(output, format);
}

void
JTextEditor::WritePlainText
	(
	ostream&				output,
	const PlainTextFormat	format
	)
	const
{
	if (format == kUNIXText)
		{
		itsBuffer->Print(output);
		return;
		}

	const JCharacter* newlineStr = NULL;
	if (format == kDOSText)
		{
		newlineStr = kDOSNewline;
		}
	else if (format == kMacintoshText)
		{
		newlineStr = kMacintoshNewline;
		}
	assert( newlineStr != NULL );

	const JCharacter* buffer = itsBuffer->GetCString();
	const JSize bufLength    = itsBuffer->GetLength();
	JIndex start             = 0;
	for (JIndex i=0; i<bufLength; i++)
		{
		if (buffer[i] == '\n')
			{
			if (start < i)
				{
				output.write(buffer + start, i - start);
				}
			output << newlineStr;
			start = i+1;
			}
		}

	if (start < bufLength)
		{
		output.write(buffer + start, bufLength - start);
		}
}

/******************************************************************************
 ReadUNIXManOutput

	Returns kFalse if cancelled.

	"_\b_" => underlined space
	"_\bc" => underlined c
	"c\bc" => bold c

 ******************************************************************************/

JBoolean
JTextEditor::ReadUNIXManOutput
	(
	istream&		input,
	const JBoolean	allowCancel
	)
{
	TEDisplayBusyCursor();

	itsBuffer->Clear();
	itsStyles->RemoveAll();

	const JCharacter* fontName = GetDefaultFontName();

	Font boldFont = itsDefFont;
	boldFont.style.bold = kTrue;
	boldFont.id =
		itsFontMgr->GetFontID(fontName, boldFont.size, boldFont.style);

	Font ulFont = itsDefFont;
	ulFont.style.underlineCount = 1;
	ulFont.id =
		itsFontMgr->GetFontID(fontName, ulFont.size, ulFont.style);

	JBoolean cancelled = kFalse;
	JLatentPG pg(100);
	pg.VariableLengthProcessBeginning("Reading man page...",
									  allowCancel, kFalse);

	input >> ws;
	while (!cancelled)
		{
		JCharacter c = input.get();
		if (input.eof() || input.fail())
			{
			break;
			}

		if (c == '_' && input.peek() == '\b')
			{
			input.ignore();
			c = input.get();
			if (input.eof() || input.fail())
				{
				break;
				}

			itsBuffer->AppendCharacter(c == '_' ? JCharacter(' ') : c);   // cast required for VCPP
			itsStyles->AppendElement(ulFont);
			}
		else if (c == '\b' && !itsBuffer->IsEmpty())
			{
			c = input.get();
			if (input.eof() || input.fail())
				{
				break;
				}

			if (c == itsBuffer->GetLastCharacter())
				{
				itsStyles->SetElement(itsStyles->GetElementCount(), boldFont);
				}
			else
				{
				input.putback(c);
				continue;
				}
			}
		else if (c == '\n' && itsBuffer->EndsWith("\n\n"))
			{
			// toss extra blank lines
			continue;
			}
		else
			{
			itsBuffer->AppendCharacter(c);
			itsStyles->AppendElement(itsDefFont);

			if (c == '\n')
				{
				cancelled = JNegate( pg.IncrementProgress() );
				}
			}
		}

	pg.ProcessFinished();

	// trim trailing whitespace

	while (!itsBuffer->IsEmpty() && isspace(itsBuffer->GetLastCharacter()))
		{
		const JSize length = itsBuffer->GetLength();
		itsBuffer->RemoveSubstring(length,length);
		itsStyles->RemoveElement(length);
		}

	SetText1(itsStyles);
	return JNegate(cancelled);
}

/******************************************************************************
 ReadPrivateFormat

	See WritePrivateFormat() for version information.

 ******************************************************************************/

JBoolean
JTextEditor::ReadPrivateFormat
	(
	istream& input
	)
{
	TEDisplayBusyCursor();

	JString text;
	JRunArray<Font> style;
	if (ReadPrivateFormat(input, this, &text, &style))
		{
		*itsBuffer = text;
		SetText1(&style);
		return kTrue;
		}
	else
		{
		return kFalse;
		}
}

/******************************************************************************
 ReadPrivateFormat (static protected)

	See WritePrivateFormat() for version information.

 ******************************************************************************/

JBoolean
JTextEditor::ReadPrivateFormat
	(
	istream&			input,
	const JTextEditor*	te,
	JString*			text,
	JRunArray<Font>*	style
	)
{
JIndex i;

	text->Clear();
	style->RemoveAll();

	// version

	JFileVersion vers;
	input >> vers;

	if (vers > kCurrentPrivateFormatVersion)
		{
		return kFalse;
		}

	// text

	JSize textLength;
	input >> textLength;
	input.ignore(1);
	if (textLength > 0)
		{
		text->Read(input, textLength);
		}

	// list of font names

	JSize fontCount;
	input >> fontCount;

	JPtrArray<JString> fontList;
	for (i=1; i<=fontCount; i++)
		{
		JString* name = new JString;
		assert( name != NULL );
		input >> *name;
		fontList.Append(name);
		}

	// list of rgb values

	JSize colorCount;
	input >> colorCount;

	JArray<JColorIndex> colorList;

	JRGB color;
	for (i=1; i<=colorCount; i++)
		{
		input >> color;
		colorList.AppendElement(te->RGBToColorIndex(color));
		}

	// styles

	JSize runCount;
	input >> runCount;

	const JFontManager* fontMgr = te->TEGetFontManager();

	for (i=1; i<=runCount; i++)
		{
		JSize charCount;
		input >> charCount;

		JIndex fontIndex;
		input >> fontIndex;

		Font f;
		input >> f.size;
		input >> f.style.bold >> f.style.italic >> f.style.strike;
		input >> f.style.underlineCount;

		JIndex colorIndex;
		input >> colorIndex;
		f.style.color = colorList.GetElement(colorIndex);

		const JString* name = fontList.NthElement(fontIndex);
		f.id = fontMgr->GetFontID(*name, f.size, f.style);

		style->AppendElements(f, charCount);
		}

	// clean up

	fontList.DeleteAll();
	return kTrue;
}

/******************************************************************************
 WritePrivateFormat

 ******************************************************************************/

void
JTextEditor::WritePrivateFormat
	(
	ostream& output
	)
	const
{
	WritePrivateFormat(output, kCurrentPrivateFormatVersion,
					   1, itsBuffer->GetLength());
}

/******************************************************************************
 WritePrivateFormat (protected)

 ******************************************************************************/

void
JTextEditor::WritePrivateFormat
	(
	ostream&			output,
	const JFileVersion	vers,
	const JIndex		startIndex,
	const JIndex		endIndex
	)
	const
{
	WritePrivateFormat(output, this, vers, *itsBuffer, *itsStyles,
					   startIndex, endIndex);
}

void
JTextEditor::WritePrivateFormat
	(
	ostream&				output,
	const JFileVersion		vers,
	const JString&			text,
	const JRunArray<Font>&	style
	)
	const
{
	if (!text.IsEmpty() && text.GetLength() == style.GetElementCount())
		{
		WritePrivateFormat(output, this, vers, text, style,
						   1, text.GetLength());
		}
}

/******************************************************************************
 WriteClipboardPrivateFormat (protected)

	Returns kFalse if the clipboard is empty.

 ******************************************************************************/

JBoolean
JTextEditor::WriteClipboardPrivateFormat
	(
	ostream&			output,
	const JFileVersion	vers
	)
	const
{
	if (itsClipText != NULL && itsClipStyle != NULL &&
		!itsClipText->IsEmpty())
		{
		WritePrivateFormat(output, this, vers, *itsClipText, *itsClipStyle,
						   1, itsClipText->GetLength());
		return kTrue;
		}
	else
		{
		return kFalse;
		}
}

/******************************************************************************
 WriteDragClipPrivateFormat (protected)

	Returns kFalse if the drag clipboard is empty.

 ******************************************************************************/

JBoolean
JTextEditor::WriteDragClipPrivateFormat
	(
	ostream&			output,
	const JFileVersion	vers
	)
	const
{
	const JString* text;
	const JRunArray<Font>* style;
	if (GetDragClip(&text, &style))
		{
		WritePrivateFormat(output, this, vers, *text, *style,
						   1, text->GetLength());
		return kTrue;
		}
	else
		{
		return kFalse;
		}
}

/******************************************************************************
 WritePrivateFormat (static protected)

	We have to be able to write each version because this is what we
	put on the clipboard.

	version 1:  initial version.

 ******************************************************************************/

void
JTextEditor::WritePrivateFormat
	(
	ostream&				output,
	const JTextEditor*		te,
	const JFileVersion		vers,
	const JString&			text,
	const JRunArray<Font>&	style,
	const JIndex			startIndex,
	const JIndex			endIndex
	)
{
	assert( text.IndexValid(startIndex) );
	assert( text.IndexValid(endIndex) );
	assert( startIndex <= endIndex );
	assert( vers <= kCurrentPrivateFormatVersion );

	// write version

	output << vers;

	// write text efficiently

	const JSize textLength = endIndex-startIndex+1;
	output << ' ' << textLength << ' ';
	output.write(text.GetCString() + startIndex-1, textLength);

	// build lists of font names and colors

	const JFontManager* fontMgr = te->TEGetFontManager();

	JSize styleRunCount = 0;
	JPtrArray<JString> fontList;
	fontList.SetCompareFunction(JCompareStringsCaseSensitive);
	JArray<JColorIndex> colorList;
	colorList.SetCompareFunction(JCompareIndices);

	JIndex startRunIndex, startFirstInRun;
	JBoolean found = style.FindRun(startIndex, &startRunIndex, &startFirstInRun);
	assert( found );

	JIndex i          = startIndex;
	JIndex runIndex   = startRunIndex;
	JIndex firstInRun = startFirstInRun;
	do
		{
		const Font& f    = style.GetRunDataRef(runIndex);
		JString fontName = fontMgr->GetFontName(f.id);
		const JIndex fontIndex =
			fontList.SearchSorted1(&fontName, JOrderedSetT::kAnyMatch, &found);
		if (!found)
			{
			JString* name = new JString(fontName);
			assert( name != NULL );
			fontList.InsertAtIndex(fontIndex, name);
			}

		const JIndex colorIndex =
			colorList.SearchSorted1(f.style.color, JOrderedSetT::kAnyMatch, &found);
		if (!found)
			{
			colorList.InsertElementAtIndex(colorIndex, f.style.color);
			}

		i += style.GetRunLength(runIndex) - (i - firstInRun);
		runIndex++;
		firstInRun = i;
		styleRunCount++;
		}
		while (i <= endIndex);

	// write list of font names

	const JSize fontCount = fontList.GetElementCount();
	output << ' ' << fontCount;

	for (i=1; i<=fontCount; i++)
		{
		output << ' ' << *(fontList.NthElement(i));
		}

	// write list of rgb values

	const JSize colorCount = colorList.GetElementCount();
	output << ' ' << colorCount;

	for (i=1; i<=colorCount; i++)
		{
		output << ' ' << (te->itsColormap)->GetRGB(colorList.GetElement(i));
		}

	// write styles

	output << ' ' << styleRunCount;

	i          = startIndex;
	runIndex   = startRunIndex;
	firstInRun = startFirstInRun;
	do
		{
		JSize charCount = style.GetRunLength(runIndex) - (i - firstInRun);
		if (endIndex < i + charCount - 1)
			{
			charCount = endIndex - i + 1;
			}

		const Font& f    = style.GetRunDataRef(runIndex);
		JString fontName = fontMgr->GetFontName(f.id);

		JIndex fontIndex;
		found = fontList.SearchSorted(&fontName, JOrderedSetT::kAnyMatch, &fontIndex);
		assert( found );

		JIndex colorIndex;
		found = colorList.SearchSorted(f.style.color, JOrderedSetT::kAnyMatch, &colorIndex);
		assert( found );

		output << ' ' << charCount;
		output << ' ' << fontIndex;
		output << ' ' << f.size;
		output << ' ' << f.style.bold << f.style.italic << f.style.strike;
		output << ' ' << f.style.underlineCount;
		output << ' ' << colorIndex;

		i += charCount;
		runIndex++;
		firstInRun = i;
		}
		while (i <= endIndex);

	// clean up

	fontList.DeleteAll();
}

/******************************************************************************
 ColorNameToColorIndex (private)

 ******************************************************************************/

JColorIndex
JTextEditor::ColorNameToColorIndex
	(
	const JCharacter* name
	)
	const
{
	JColorIndex colorIndex;
	if (itsColormap->AllocateStaticNamedColor(name, &colorIndex))
		{
		if (itsCustomColors == NULL)
			{
			JTextEditor* te = const_cast<JTextEditor*>(this);
			te->itsCustomColors = new JArray<JColorIndex>;
			assert( itsCustomColors != NULL );
			(te->itsCustomColors)->SetCompareFunction(JCompareIndices);
			}
		if (!itsCustomColors->InsertSorted(colorIndex, kFalse))
			{
			itsColormap->DeallocateColor(colorIndex);
			}
		return colorIndex;
		}
	else
		{
		return itsColormap->GetBlackColor();
		}
}

/******************************************************************************
 RGBToColorIndex (private)

 ******************************************************************************/

JColorIndex
JTextEditor::RGBToColorIndex
	(
	const JRGB& color
	)
	const
{
	JColorIndex colorIndex;
	if (itsColormap->AllocateStaticColor(color, &colorIndex))
		{
		if (itsCustomColors == NULL)
			{
			JTextEditor* te = const_cast<JTextEditor*>(this);
			te->itsCustomColors = new JArray<JColorIndex>;
			assert( itsCustomColors != NULL );
			(te->itsCustomColors)->SetCompareFunction(JCompareIndices);
			}
		if (!itsCustomColors->InsertSorted(colorIndex, kFalse))
			{
			itsColormap->DeallocateColor(colorIndex);
			}
		return colorIndex;
		}
	else
		{
		return itsColormap->GetBlackColor();
		}
}

/******************************************************************************
 HTML

 ******************************************************************************/

#define JTemplateType JTextEditor::HTMLListType
#include <JArray.tmpls>
#undef JTemplateType

#define JTemplateType JArray<JTextEditor::HTMLListType>,JTextEditor::HTMLListType
#include <JStack.tmpls>
#undef JTemplateType

static const JSize kHTMLPointSize[]      = { 8, 10, 12, 14, 18, 18, 24 };
static const JSize kHTMLHeaderFontSize[] = { 18, 18, 14, 12, 10, 8 };

const JSize kBigHTMLPointSize     = 14;
const JSize kDefaultHTMLPointSize = 12;
const JSize kSmallHTMLPointSize   = 10;

/******************************************************************************
 AppendCharsForHTML (private)

 ******************************************************************************/

inline void
JTextEditor::AppendCharsForHTML
	(
	const JCharacter*			text,
	const JTextEditor::Font&	f
	)
{
	itsBuffer->Append(text);
	itsStyles->AppendElements(f, strlen(text));
}

inline void
JTextEditor::AppendCharsForHTML
	(
	const JString&				text,
	const JTextEditor::Font&	f
	)
{
	itsBuffer->Append(text);
	itsStyles->AppendElements(f, text.GetLength());
}

/******************************************************************************
 ReadHTML

	Parses HTML text and approximates the formatting.

 ******************************************************************************/

void
JTextEditor::ReadHTML
	(
	istream& input
	)
{
	assert( itsHTMLLexerState == NULL );

	TEDisplayBusyCursor();
	PrepareToReadHTML();

	itsBuffer->Clear();
	itsStyles->RemoveAll();

	HTMLLexerState state(this);
	itsHTMLLexerState = &state;

	JTEHTMLScanner scanner(this);
	scanner.LexHTML(input);

	itsHTMLLexerState = NULL;

	ClearUndo();
	SetCaretLocation(1);
	RecalcAll(kTrue);
	Broadcast(TextSet());

	ReadHTMLFinished();
}

/******************************************************************************
 Handle HTML objects (virtual protected)

	Even though these are not overrides of JHTMLScanner functions, they
	need to be virtual so derived classes can augment them.

 ******************************************************************************/

void
JTextEditor::PrepareToReadHTML()
{
}

void
JTextEditor::ReadHTMLFinished()
{
}

void
JTextEditor::HandleHTMLWord
	(
	const JCharacter*	word,
	const JIndexRange&	range
	)
{
	if (!itsHTMLLexerState->inDocHeader)
		{
		AppendTextForHTML(word);
		}
}

void
JTextEditor::HandleHTMLWhitespace
	(
	const JCharacter*	space,
	const JIndexRange&	range
	)
{
	if (itsHTMLLexerState->inPreformatBlock)
		{
		AppendTextForHTML(space);
		}
	else if (!itsHTMLLexerState->inDocHeader)
		{
		itsHTMLLexerState->appendWS = kTrue;
		}
}

void
JTextEditor::HandleHTMLTag
	(
	const JString&					name,
	const JStringPtrMap<JString>&	attr,
	const JIndexRange&				range
	)
{
	if (name.GetFirstCharacter() == '/')
		{
		JString s = name;
		s.RemoveSubstring(1,1);
		HandleHTMLOffCmd(s, attr);
		}
	else
		{
		HandleHTMLOnCmd(name, attr);
		}
}

void
JTextEditor::HandleHTMLComment
	(
	const JIndexRange& range
	)
{
}

void
JTextEditor::HandleHTMLError
	(
	const JHTMLScanner::HTMLError	err,
	const JCharacter*				errStr,
	const JIndexRange&				range
	)
{
	itsHTMLLexerState->ReportError(errStr);
}

/******************************************************************************
 HandleHTMLOnCmd (private)

 ******************************************************************************/

void
JTextEditor::HandleHTMLOnCmd
	(
	const JString&					cmd,
	const JStringPtrMap<JString>&	attr
	)
{
	// new paragraph

	if (cmd == "p")
		{
		AppendNewlinesForHTML(2);
		}

	// line break

	else if (cmd == "br")
		{
		AppendCharsForHTML("\n", itsHTMLLexerState->font);
		(itsHTMLLexerState->newlineCount)++;
		}

	// header (h1 - h6)

	else if (cmd.GetLength() == 2 && cmd.GetCharacter(1) == 'h' &&
			 ('1' <= cmd.GetCharacter(2) && cmd.GetCharacter(2) <= '6'))
		{
		AppendNewlinesForHTML(2);

		itsHTMLLexerState->PushCurrentFont();
		(itsHTMLLexerState->font).size       = kHTMLHeaderFontSize[ cmd.GetCharacter(2) - '1' ];
		(itsHTMLLexerState->font).style.bold = kTrue;
		itsHTMLLexerState->UpdateFontID();
		}

	// bold

	else if (cmd == "b" || cmd == "strong")
		{
		itsHTMLLexerState->PushCurrentFont();
		(itsHTMLLexerState->font).style.bold = kTrue;
		itsHTMLLexerState->UpdateFontID();
		}

	// italic

	else if (cmd == "i" || cmd == "em" ||
			 cmd == "cite" || cmd == "var" || cmd == "ins")
		{
		itsHTMLLexerState->PushCurrentFont();
		(itsHTMLLexerState->font).style.italic = kTrue;
		itsHTMLLexerState->UpdateFontID();
		}

	// underline

	else if (cmd == "u")
		{
		itsHTMLLexerState->PushCurrentFont();
		(itsHTMLLexerState->font).style.underlineCount = 1;
		itsHTMLLexerState->UpdateFontID();
		}

	// strike

	else if (cmd == "strike" || cmd == "s" || cmd == "del")
		{
		itsHTMLLexerState->PushCurrentFont();
		(itsHTMLLexerState->font).style.strike = kTrue;
		itsHTMLLexerState->UpdateFontID();
		}

	// arbitrary font

	else if (cmd == "font")
		{
		itsHTMLLexerState->PushCurrentFont();
		SetFontForHTML(attr);
		}

	// monospace / preformatted

	else if (cmd == "tt" || cmd == "samp" || cmd == "kbd" || cmd == "code" ||
			 cmd == "pre")
		{
		if (cmd == "pre")
			{
			AppendNewlinesForHTML(2);
			itsHTMLLexerState->inPreformatBlock = kTrue;
			}

		itsHTMLLexerState->PushCurrentFont();
		itsHTMLLexerState->fontName         = JGetMonospaceFontName();
		itsHTMLLexerState->font.size        = kDefaultHTMLPointSize;
		itsHTMLLexerState->font.style.color = itsColormap->GetBlackColor();
		itsHTMLLexerState->UpdateFontID();
		}

	// unordered list

	else if (cmd == "dir" || cmd == "menu" || cmd == "ul")
		{
		itsHTMLLexerState->NewList(kHTMLUnordList);
		}

	// ordered list

	else if (cmd == "ol")
		{
		itsHTMLLexerState->NewList(kHTMLOrdList);

		JString* valueStr;
		JIndex startIndex;
		if (attr.GetElement("start", &valueStr) &&
			valueStr != NULL &&
			valueStr->ConvertToUInt(&startIndex) &&
			startIndex > 0)
			{
			itsHTMLLexerState->listIndex = startIndex - 1;
			}
		}

	// list item

	else if (cmd == "li")
		{
		if (itsHTMLLexerState->listType == kHTMLUnordList)
			{
			itsHTMLLexerState->NewListItem();
			AppendCharsForHTML("* ", itsHTMLLexerState->GetWSFont());
			}
		else if (itsHTMLLexerState->listType == kHTMLOrdList)
			{
			itsHTMLLexerState->NewListItem();

			JString* valueStr;
			JInteger newIndex;
			if (attr.GetElement("value", &valueStr) &&
				valueStr != NULL &&
				valueStr->ConvertToInteger(&newIndex))
				{
				itsHTMLLexerState->listIndex = newIndex;
				}

			JString str(itsHTMLLexerState->listIndex, 0);
			str += ". ";
			AppendCharsForHTML(str, itsHTMLLexerState->GetWSFont());
			}
		else
			{
			itsHTMLLexerState->ReportError("*** list element found outside list ***");
			}
		}

	// definition list

	else if (cmd == "dl")
		{
		itsHTMLLexerState->NewList(kHTMLDefTermList);
		}

	// term / definition

	else if (cmd == "dt" &&
			 (itsHTMLLexerState->listType == kHTMLDefTermList ||
			  itsHTMLLexerState->listType == kHTMLDefDataList))
		{
		itsHTMLLexerState->listType = kHTMLDefTermList;
		itsHTMLLexerState->NewListItem();
		}
	else if (cmd == "dd" &&
			 (itsHTMLLexerState->listType == kHTMLDefTermList ||
			  itsHTMLLexerState->listType == kHTMLDefDataList))
		{
		itsHTMLLexerState->listType = kHTMLDefDataList;
		itsHTMLLexerState->NewListItem();
		}
	else if (cmd == "dt" || cmd == "dd")
		{
		itsHTMLLexerState->ReportError("*** list element found outside list ***");
		}

	// horizontal rule

	else if (cmd == "hr")
		{
		AppendNewlinesForHTML(1);
		AppendTextForHTML("----------");
		AppendNewlinesForHTML(1);
		}

	// blockquote

	else if (cmd == "blockquote")
		{
		AppendNewlinesForHTML(2);
		(itsHTMLLexerState->indentCount)++;
		}

	// address

	else if (cmd == "address")
		{
		AppendNewlinesForHTML(1);

		itsHTMLLexerState->PushCurrentFont();
		(itsHTMLLexerState->font).style.italic = kTrue;
		itsHTMLLexerState->UpdateFontID();
		}

	// image

	else if (cmd == "img")
		{
		const Font saveFont     = itsHTMLLexerState->font;
		itsHTMLLexerState->font = itsHTMLLexerState->blankLineFont;

		JString* valueStr;
		if ((attr.GetElement("alt", &valueStr) &&
			 valueStr != NULL && !valueStr->IsEmpty()) ||
			(attr.GetElement("src", &valueStr) &&
			 valueStr != NULL && !valueStr->IsEmpty()))
			{
			JString s = *valueStr;
			s.PrependCharacter('[');
			s.AppendCharacter(']');
 			AppendTextForHTML(s);
			}
		else
			{
			AppendTextForHTML("[image]");
			}

		itsHTMLLexerState->font = saveFont;
		}

	// big font size

	else if (cmd == "big")
		{
		itsHTMLLexerState->PushCurrentFont();
		(itsHTMLLexerState->font).size = kBigHTMLPointSize;
		itsHTMLLexerState->UpdateFontID();
		}

	// big font size

	else if (cmd == "small")
		{
		itsHTMLLexerState->PushCurrentFont();
		(itsHTMLLexerState->font).size = kSmallHTMLPointSize;
		itsHTMLLexerState->UpdateFontID();
		}

	// document header

	else if (cmd == "head")
		{
		itsHTMLLexerState->inDocHeader = kTrue;
		}

	// division inside document

	else if (cmd == "div")
		{
		AppendNewlinesForHTML(1);
		}
}

/******************************************************************************
 SetFontForHTML (private)

 ******************************************************************************/

void
JTextEditor::SetFontForHTML
	(
	const JStringPtrMap<JString>& attr
	)
{
	// name

	JString* valueStr;
	if (attr.GetElement("face", &valueStr))
		{
		if (valueStr != NULL && !valueStr->IsEmpty())
			{
			itsHTMLLexerState->fontName = *valueStr;
			}
		else
			{
			itsHTMLLexerState->ReportError("*** empty font face ***");
			}
		}

	// size

	if (attr.GetElement("size", &valueStr))
		{
		if (valueStr != NULL && !valueStr->IsEmpty())
			{
			JBoolean relPlus  = kFalse;
			JBoolean relMinus = kFalse;
			if (valueStr->GetFirstCharacter() == '+')
				{
				valueStr->RemoveSubstring(1,1);
				relPlus = kTrue;
				}
			else if (valueStr->GetFirstCharacter() == '-')
				{
				valueStr->RemoveSubstring(1,1);
				relMinus = kTrue;
				}

			JInteger value;
			if (valueStr->ConvertToInteger(&value))
				{
				if (relPlus)
					{
					value += 3;
					}
				else if (relMinus)
					{
					value = 3 - value;
					}

				if (value < 1)
					{
					value = 1;
					}
				else if (value > 7)
					{
					value = 7;
					}

				(itsHTMLLexerState->font).size = kHTMLPointSize[value-1];
				}
			}
		else
			{
			itsHTMLLexerState->ReportError("*** empty font size ***");
			}
		}

	// color

	if (attr.GetElement("color", &valueStr))
		{
		if (valueStr != NULL && !valueStr->IsEmpty())
			{
			(itsHTMLLexerState->font).style.color = ColorNameToColorIndex(*valueStr);
			}
		else
			{
			itsHTMLLexerState->ReportError("*** empty font color ***");
			}
		}

	// update font id

	itsHTMLLexerState->UpdateFontID();
}

/******************************************************************************
 HandleHTMLOffCmd (private)

	The leading slash is assumed to have been removed.

 ******************************************************************************/

void
JTextEditor::HandleHTMLOffCmd
	(
	const JString&					cmd,
	const JStringPtrMap<JString>&	attr
	)
{
	// header (h1 - h6)

	if (cmd.GetLength() == 2 && cmd.GetCharacter(1) == 'h' &&
		('1' <= cmd.GetCharacter(2) && cmd.GetCharacter(2) <= '6'))
		{
		AppendNewlinesForHTML(2);
		itsHTMLLexerState->PopFont();
		}

	// close style

	else if (cmd == "b" || cmd == "strong" ||
			 cmd == "i" || cmd == "em" ||
			 cmd == "cite" || cmd == "var" || cmd == "ins" ||
			 cmd == "u" ||
			 cmd == "strike" || cmd == "s" || cmd == "del" ||
			 cmd == "tt" || cmd == "samp" || cmd == "kbd" || cmd == "code" ||
			 cmd == "font" ||
			 cmd == "big" || cmd == "small")
		{
		itsHTMLLexerState->PopFont();
		}

	// preformatted

	else if (cmd == "pre")
		{
		if (itsHTMLLexerState->newlineCount == 0 &&
			!itsBuffer->IsEmpty() && itsBuffer->GetLastCharacter() == '\n')
			{
			itsHTMLLexerState->newlineCount = 1;
			}
		AppendNewlinesForHTML(2);

		itsHTMLLexerState->inPreformatBlock = kFalse;
		itsHTMLLexerState->PopFont();
		}

	// list

	else if (cmd == "dir" || cmd == "menu" || cmd == "ul" ||
			 cmd == "ol" || cmd == "dl")
		{
		itsHTMLLexerState->EndList();
		}

	// blockquote

	else if (cmd == "blockquote")
		{
		if (itsHTMLLexerState->indentCount == 0)
			{
			itsHTMLLexerState->ReportError("*** unbalanced closing blockquote ***");
			}
		else
			{
			AppendNewlinesForHTML(2);
			(itsHTMLLexerState->indentCount)--;
			}
		}

	// address

	else if (cmd == "address")
		{
		AppendNewlinesForHTML(1);
		itsHTMLLexerState->PopFont();
		}

	// table row

	else if (cmd == "tr")
		{
		AppendNewlinesForHTML(1);
		}

	// document header

	else if (cmd == "head")
		{
		itsHTMLLexerState->inDocHeader = kFalse;
		}

	// division inside document

	else if (cmd == "div")
		{
		AppendNewlinesForHTML(1);
		}
}

/******************************************************************************
 AppendTextForHTML (private)

	Appends the given text to the buffer, accounting for the need for
	indenting and whitespace.

 ******************************************************************************/

void
JTextEditor::AppendTextForHTML
	(
	const JString& text
	)
{
	if (!text.Contains("\n"))
		{
		AppendTextForHTML1(text);
		return;
		}

	JString s = text;

	JIndex newlineIndex;
	while (s.LocateSubstring("\n", &newlineIndex) &&
		   newlineIndex < s.GetLength())
		{
		AppendTextForHTML1(s.GetSubstring(1, newlineIndex));
		s.RemoveSubstring(1, newlineIndex);
		}

	AppendTextForHTML1(s);
}

void
JTextEditor::AppendTextForHTML1
	(
	const JString& text
	)
{
	if (text.IsEmpty())
		{
		return;
		}

	const Font wsFont = itsHTMLLexerState->GetWSFont();

	JCharacter lastChar = '\0';
	if (!itsBuffer->IsEmpty())
		{
		lastChar = itsBuffer->GetLastCharacter();
		}

	if (lastChar == '\n')
		{
		itsHTMLLexerState->IndentForListItem(wsFont);

		if (itsHTMLLexerState->listType == JTextEditor::kHTMLUnordList)
			{
			AppendCharsForHTML("  ", wsFont);
			}
		else if (itsHTMLLexerState->listType == JTextEditor::kHTMLOrdList)
			{
			JString str(itsHTMLLexerState->listIndex, 0);
			for (JIndex i=1; i<=str.GetLength(); i++)
				{
				str.SetCharacter(i, ' ');
				}
			str += "  ";
			AppendCharsForHTML(str, wsFont);
			}
		}
	else if (lastChar != '\0' && !isspace(lastChar) && itsHTMLLexerState->appendWS)
		{
		AppendCharsForHTML(" ",
			CalcWSFont(itsStyles->GetLastElement(), itsHTMLLexerState->font));
		}
	itsHTMLLexerState->appendWS = kFalse;

	AppendCharsForHTML(text, itsHTMLLexerState->font);
	itsHTMLLexerState->newlineCount = 0;
}

/******************************************************************************
 AppendNewlinesForHTML (private)

	Appends newlines to the buffer up to the requested number.

 ******************************************************************************/

void
JTextEditor::AppendNewlinesForHTML
	(
	const JSize count
	)
{
	if (!itsBuffer->IsEmpty())
		{
		const JTextEditor::Font* f = &(itsHTMLLexerState->font);
		while (itsHTMLLexerState->newlineCount < count)
			{
			AppendCharsForHTML("\n", *f);
			(itsHTMLLexerState->newlineCount)++;
			f = &(itsHTMLLexerState->blankLineFont);
			}
		}
}

/******************************************************************************
 CalcWSFont (private)

	Calculates the appropriate style for whitespace between two styled words.
	We don't recalculate the font id because we only change underline and strike.

 ******************************************************************************/

JTextEditor::Font
JTextEditor::CalcWSFont
	(
	const Font& prevFont,
	const Font& nextFont
	)
	const
{
	Font f = nextFont;

	const JBoolean ulMatch =
		JI2B( prevFont.style.underlineCount == nextFont.style.underlineCount );

	const JBoolean sMatch =
		JI2B( prevFont.style.strike == nextFont.style.strike );

	if (!ulMatch && !sMatch &&
		prevFont.style.underlineCount == 0 && !prevFont.style.strike)
		{
		f = prevFont;
		}
	else if (!ulMatch && !sMatch &&
			 nextFont.style.underlineCount == 0 && !nextFont.style.strike)
		{
		// f = nextFont;
		}
	else if (!ulMatch && !sMatch)
		{
		f.style.underlineCount = 0;
		f.style.strike         = kFalse;
		}
	else if (!ulMatch && prevFont.style.underlineCount == 0)
		{
		f = prevFont;
		}
	else if (!ulMatch && nextFont.style.underlineCount == 0)
		{
		// f = nextFont;
		}
	else if (!ulMatch)
		{
		f.style.underlineCount = 0;
		}
	else if (!sMatch && !prevFont.style.strike)
		{
		f = prevFont;
		}
	else if (!sMatch && !nextFont.style.strike)
		{
		// f = nextFont;
		}

	return f;
}

/******************************************************************************
 HTMLLexerState functions

 ******************************************************************************/

JTextEditor::HTMLLexerState::HTMLLexerState
	(
	JTextEditor* editor
	)
	:
	te(editor),
	font(editor->itsFontMgr->GetFontID(JGetDefaultFontName(), 12, JFontStyle()),
		 12, JFontStyle()),
	fontName(JGetDefaultFontName()),
	blankLineFont(font),
	listType(kHTMLNoList),
	listIndex(0),
	indentCount(0),
	newlineCount(0),
	appendWS(kFalse),
	inPreformatBlock(kFalse),
	inDocHeader(kFalse)
{
}

void
JTextEditor::HTMLLexerState::PushCurrentFont()
{
	fontStack.Push(font);

	JString* name = new JString(fontName);
	assert( name != NULL );
	fontNameStack.Push(name);
}

JBoolean
JTextEditor::HTMLLexerState::PopFont()
{
	if (fontStack.IsEmpty())
		{
		ReportError("*** unbalanced closing style tag ***");
		fontNameStack.ClearDelete();
		return kFalse;
		}
	else
		{
		JString* name = fontNameStack.Pop();
		fontName      = *name;
		delete name;

		font = fontStack.Pop();
		UpdateFontID();
		return kTrue;
		}
}

void
JTextEditor::HTMLLexerState::UpdateFontID()
{
	font.id = te->itsFontMgr->GetFontID(fontName, font.size, font.style);
}

JTextEditor::Font
JTextEditor::HTMLLexerState::GetWSFont()
{
	JTextEditor::Font wsFont(0, font.size, JFontStyle());
	wsFont.id = te->itsFontMgr->GetFontID(fontName, wsFont.size, wsFont.style);
	return wsFont;
}

void
JTextEditor::HTMLLexerState::NewList
	(
	const HTMLListType type
	)
{
	assert( type != kHTMLNoList );

	if (listType == kHTMLNoList)
		{
		te->AppendNewlinesForHTML(2);
		}
	else
		{
		te->AppendNewlinesForHTML(1);
		}

	listTypeStack.Push(listType);
	listIndexStack.Push(listIndex);

	listType  = type;
	listIndex = 0;
}

void
JTextEditor::HTMLLexerState::NewListItem()
{
	te->AppendNewlinesForHTML(1);
	IndentForListItem(GetWSFont());
	listIndex++;
	appendWS = kFalse;
}

void
JTextEditor::HTMLLexerState::IndentForListItem
	(
	const Font& wsFont
	)
{
JIndex i;

	for (i=1; i<=indentCount; i++)
		{
		te->AppendCharsForHTML("\t", wsFont);
		}

	const JSize listIndentDepth = listTypeStack.GetElementCount();
	for (i=1; i<listIndentDepth; i++)
		{
		if (listTypeStack.Peek(i) != kHTMLDefTermList)
			{
			te->AppendCharsForHTML("\t", wsFont);
			}
		}
	if (listType != kHTMLNoList && listType != kHTMLDefTermList)
		{
		te->AppendCharsForHTML("\t", wsFont);
		}

	newlineCount = 0;
}

JBoolean
JTextEditor::HTMLLexerState::EndList()
{
	if (listTypeStack.IsEmpty())
		{
		ReportError("*** unbalanced closing list tag ***");
		return kFalse;
		}
	else
		{
		listType  = listTypeStack.Pop();
		listIndex = listIndexStack.Pop();

		if (listType == kHTMLNoList)
			{
			te->AppendNewlinesForHTML(2);
			}

		return kTrue;
		}
}

void
JTextEditor::HTMLLexerState::ReportError
	(
	const JCharacter* errStr
	)
{
	const Font wsFont = GetWSFont();
	te->AppendCharsForHTML("\n",   wsFont);
	te->AppendCharsForHTML(errStr, wsFont);
	te->AppendCharsForHTML("\n",   wsFont);
	newlineCount = 1;
}

/******************************************************************************
 Search and replace

 ******************************************************************************/

/******************************************************************************
 SearchForward

	Look for the next match beyond the current position.
	If we find it, we select it and return kTrue.

 ******************************************************************************/

JBoolean
JTextEditor::SearchForward
	(
	const JCharacter*	searchStr,
	const JBoolean		caseSensitive,
	const JBoolean		entireWord,
	const JBoolean		wrapSearch,
	JBoolean*			wrapped
	)
{
	JIndex startIndex;
	if (!itsSelection.IsEmpty())
		{
		startIndex = itsSelection.last + 1;
		}
	else
		{
		startIndex = itsCaretLoc.charIndex;
		}

	const JIndex origStartIndex = startIndex;

	*wrapped = kFalse;
	const JSize bufLength = itsBuffer->GetLength();
	if (startIndex > bufLength && wrapSearch)
		{
		startIndex = 1;
		*wrapped   = kTrue;
		}
	else if (startIndex > bufLength)
		{
		return kFalse;
		}

	const JSize searchLength = strlen(searchStr);

	JBoolean found = kFalse;
	while (1)
		{
		found = itsBuffer->LocateNextSubstring(searchStr, searchLength,
											   caseSensitive, &startIndex);
		if (found && entireWord)
			{
			found = IsEntireWord(startIndex, startIndex + searchLength - 1);
			}

		if (found)
			{
			break;
			}

		startIndex++;
		if (!found && startIndex > bufLength && wrapSearch && !(*wrapped))
			{
			startIndex = 1;
			*wrapped   = kTrue;
			}
		else if ((!found && startIndex > bufLength) ||
				 (!found && *wrapped && startIndex >= origStartIndex))
			{
			break;
			}
		}

	if (found)
		{
		SetSelection(startIndex, startIndex + searchLength - 1);
		return kTrue;
		}
	else
		{
		return kFalse;
		}
}

/******************************************************************************
 SearchBackward

	Look for the match before the current position.
	If we find it, we select it and return kTrue.

 ******************************************************************************/

JBoolean
JTextEditor::SearchBackward
	(
	const JCharacter*	searchStr,
	const JBoolean		caseSensitive,
	const JBoolean		entireWord,
	const JBoolean		wrapSearch,
	JBoolean*			wrapped
	)
{
	JIndex startIndex;
	if (!itsSelection.IsEmpty())
		{
		startIndex = itsSelection.first;
		}
	else
		{
		startIndex = itsCaretLoc.charIndex;
		}
	startIndex--;

	const JIndex origStartIndex = startIndex;

	*wrapped = kFalse;
	const JSize bufLength = itsBuffer->GetLength();
	if (startIndex == 0 && wrapSearch)
		{
		startIndex = bufLength;
		*wrapped   = kTrue;
		}
	else if (startIndex == 0)
		{
		return kFalse;
		}

	const JSize searchLength = strlen(searchStr);

	JBoolean found = kFalse;
	while (1)
		{
		found = itsBuffer->LocatePrevSubstring(searchStr, searchLength,
											   caseSensitive, &startIndex);
		if (found && entireWord)
			{
			found = IsEntireWord(startIndex, startIndex + searchLength - 1);
			}

		if (found)
			{
			break;
			}

		if (startIndex > 0)
			{
			startIndex--;
			}
		if (!found && startIndex == 0 && wrapSearch && !(*wrapped))
			{
			startIndex = bufLength;
			*wrapped   = kTrue;
			}
		else if ((!found && startIndex == 0) ||
				 (!found && *wrapped && startIndex <= origStartIndex))
			{
			break;
			}
		}

	if (found)
		{
		SetSelection(startIndex, startIndex + searchLength - 1);
		return kTrue;
		}
	else
		{
		return kFalse;
		}
}

/******************************************************************************
 SelectionMatches

	Returns kTrue if the current selection matches the given search criteria.
	This should always be checked before doing a replace.

 ******************************************************************************/

JBoolean
JTextEditor::SelectionMatches
	(
	const JCharacter*	searchStr,
	const JBoolean		caseSensitive,
	const JBoolean		entireWord
	)
{
	const JSize searchLength = strlen(searchStr);
	if (itsSelection.IsEmpty() ||
		itsSelection.GetLength() != searchLength ||
		(entireWord && !IsEntireWord(itsSelection)))
		{
		return kFalse;
		}

	JIndex startIndex = itsSelection.first;
	return JConvertToBoolean(
		itsBuffer->LocateNextSubstring(searchStr, searchLength,
									   caseSensitive, &startIndex) &&
		startIndex == itsSelection.first);
}

/******************************************************************************
 SearchForward

	Look for the next match beyond the current position.
	If we find it, we select it and return kTrue.

 ******************************************************************************/

JBoolean
JTextEditor::SearchForward
	(
	const JRegex&			regex,
	const JBoolean			entireWord,
	const JBoolean			wrapSearch,
	JBoolean*				wrapped,
	JArray<JIndexRange>*	submatchList
	)
{
	JIndex startIndex;
	if (!itsSelection.IsEmpty())
		{
		startIndex = itsSelection.last + 1;
		}
	else
		{
		startIndex = itsCaretLoc.charIndex;
		}

	const JIndex origStartIndex = startIndex;

	*wrapped = kFalse;
	const JSize bufLength = itsBuffer->GetLength();
	if (startIndex > bufLength && wrapSearch)
		{
		startIndex = 1;
		*wrapped   = kTrue;
		}
	else if (startIndex > bufLength)
		{
		return kFalse;
		}

	JBoolean found = kFalse;
	JIndexRange all;
	while (1)
		{
		found = regex.MatchFrom(*itsBuffer, startIndex, submatchList);
		if (found)
			{
			all        = submatchList->GetElement(1);
			startIndex = all.first;
			found      = JNegate( all.IsEmpty() );
			if (found && (!entireWord || IsEntireWord(all)))
				{
				break;
				}
			found = kFalse;
			}
		else
			{
			startIndex = bufLength+1;
			}

		startIndex++;
		if (!found && startIndex > bufLength && wrapSearch && !(*wrapped))
			{
			startIndex = 1;
			*wrapped   = kTrue;
			}
		else if (!found &&
				 (startIndex > bufLength ||
				  (*wrapped && startIndex >= origStartIndex)))
			{
			break;
			}
		}

	if (found)
		{
		SetSelection(all);
		return kTrue;
		}
	else
		{
		return kFalse;
		}
}

/******************************************************************************
 SearchBackward

	Look for the match before the current position.
	If we find it, we select it and return kTrue.

 ******************************************************************************/

JBoolean
JTextEditor::SearchBackward
	(
	const JRegex&			regex,
	const JBoolean			entireWord,
	const JBoolean			wrapSearch,
	JBoolean*				wrapped,
	JArray<JIndexRange>*	submatchList
	)
{
	JIndex startIndex;
	if (!itsSelection.IsEmpty())
		{
		startIndex = itsSelection.first;
		}
	else
		{
		startIndex = itsCaretLoc.charIndex;
		}
	startIndex--;

	const JIndex origStartIndex = startIndex;

	*wrapped = kFalse;
	const JSize bufLength = itsBuffer->GetLength();
	if (startIndex == 0 && wrapSearch)
		{
		startIndex = bufLength;
		*wrapped   = kTrue;
		}
	else if (startIndex == 0)
		{
		return kFalse;
		}

	JBoolean found = kFalse;
	JIndexRange all;
	while (1)
		{
		found = JConvertToBoolean(
			regex.MatchLastWithin(*itsBuffer, JIndexRange(1, startIndex), submatchList) > 0);
		if (found)
			{
			all        = submatchList->GetElement(1);
			startIndex = all.first;
			found      = JNegate( all.IsEmpty() );
			if (found && (!entireWord || IsEntireWord(all)))
				{
				break;
				}
			found = kFalse;
			}
		else
			{
			startIndex = 0;
			}

		if (startIndex > 0)
			{
			startIndex--;
			}
		if (!found && startIndex == 0 && wrapSearch && !(*wrapped))
			{
			startIndex = bufLength;
			*wrapped   = kTrue;
			}
		else if (!found &&
				 (startIndex == 0 ||
				  (*wrapped && startIndex <= origStartIndex)))
			{
			break;
			}
		}

	if (found)
		{
		SetSelection(all);
		return kTrue;
		}
	else
		{
		return kFalse;
		}
}

/******************************************************************************
 SelectionMatches

	Returns kTrue if the current selection matches the given search criteria.
	This should always be checked before doing a replace.

	*** submatchList is relative to the start of the selected text.

 ******************************************************************************/

JBoolean
JTextEditor::SelectionMatches
	(
	const JRegex&			regex,
	const JBoolean			entireWord,
	JArray<JIndexRange>*	submatchList
	)
{
	if (itsSelection.IsEmpty() ||
		(entireWord && !IsEntireWord(itsSelection)))
		{
		return kFalse;
		}

	const JIndexRange searchRange(1, itsSelection.GetLength());
	return JConvertToBoolean(
		regex.MatchWithin(itsBuffer->GetCString() + itsSelection.first-1,
						  searchRange, submatchList) &&
		submatchList->GetElement(1) == searchRange);
}

/******************************************************************************
 SearchForward

	Look for the next match beyond the current position.
	If we find it, we select it and return kTrue.

 ******************************************************************************/

JBoolean
JTextEditor::SearchForward
	(
	const JFontStyle&	style,
	const JBoolean		wrapSearch,
	JBoolean*			wrapped
	)
{
	JIndex startIndex;
	if (!itsSelection.IsEmpty())
		{
		startIndex = itsSelection.last + 1;
		}
	else
		{
		startIndex = itsCaretLoc.charIndex;
		}

	const JSize runCount = itsStyles->GetRunCount();
	JIndex endRun        = runCount;

	*wrapped = kFalse;
	while (1)
		{
		JIndex runIndex, firstIndexInRun;
		if (itsStyles->FindRun(startIndex, &runIndex, &firstIndexInRun))
			{
			const JIndex origStartRun = runIndex;
			JIndex startRun           = runIndex;
			if (startIndex > firstIndexInRun)
				{
				firstIndexInRun += itsStyles->GetRunLength(startRun);
				startRun++;
				}

			for (JIndex i=startRun; i<=endRun; i++)
				{
				const JTextEditor::Font& f = itsStyles->GetRunDataRef(i);
				const JSize runLength      = itsStyles->GetRunLength(i);
				if (f.style == style)
					{
					SetSelection(firstIndexInRun, firstIndexInRun + runLength-1);
					return kTrue;
					}

				firstIndexInRun += runLength;
				}

			if (wrapSearch && !(*wrapped) && origStartRun > 1)
				{
				startIndex = 1;
				endRun     = origStartRun-1;
				*wrapped   = kTrue;
				}
			else
				{
				break;
				}
			}

		else if (wrapSearch && !(*wrapped))
			{
			startIndex = 1;
			*wrapped   = kTrue;
			}
		else
			{
			break;
			}
		}

	return kFalse;
}

/******************************************************************************
 SearchBackward

	Look for the match before the current position.
	If we find it, we select it and return kTrue.

 ******************************************************************************/

JBoolean
JTextEditor::SearchBackward
	(
	const JFontStyle&	style,
	const JBoolean		wrapSearch,
	JBoolean*			wrapped
	)
{
	JIndex startIndex;
	if (!itsSelection.IsEmpty())
		{
		startIndex = itsSelection.first;
		}
	else
		{
		startIndex = itsCaretLoc.charIndex;
		}
	startIndex--;

	JIndex endRun = 1;

	*wrapped = kFalse;
	while (1)
		{
		JIndex runIndex, firstIndexInRun;
		if (itsStyles->FindRun(startIndex, &runIndex, &firstIndexInRun))
			{
			const JIndex origStartRun = runIndex;
			JIndex startRun           = runIndex;
			if (startIndex < firstIndexInRun + itsStyles->GetRunLength(startRun)-1)
				{
				startRun--;
				if (startRun > 0)
					{
					firstIndexInRun -= itsStyles->GetRunLength(startRun);
					}
				}

			for (JIndex i=startRun; i>=endRun; i--)
				{
				const JTextEditor::Font& f = itsStyles->GetRunDataRef(i);
				if (f.style == style)
					{
					SetSelection(firstIndexInRun,
								 firstIndexInRun + itsStyles->GetRunLength(i)-1);
					return kTrue;
					}

				if (i > endRun)
					{
					firstIndexInRun -= itsStyles->GetRunLength(i-1);
					}
				}

			if (wrapSearch && !(*wrapped) && origStartRun < itsStyles->GetRunCount())
				{
				startIndex = GetTextLength();
				endRun     = origStartRun+1;
				*wrapped   = kTrue;
				}
			else
				{
				break;
				}
			}

		else if (wrapSearch && !(*wrapped))
			{
			startIndex = GetTextLength();
			*wrapped   = kTrue;
			}
		else
			{
			break;
			}
		}

	return kFalse;
}

/******************************************************************************
 SetBreakCROnly

 ******************************************************************************/

void
JTextEditor::SetBreakCROnly
	(
	const JBoolean breakCROnly
	)
{
	if (breakCROnly != itsBreakCROnlyFlag)
		{
		PrivateSetBreakCROnly(breakCROnly);
		RecalcAll(kFalse);
		}
}

// private

void
JTextEditor::PrivateSetBreakCROnly
	(
	const JBoolean breakCROnly
	)
{
	itsBreakCROnlyFlag = breakCROnly;
	if (!itsBreakCROnlyFlag)
		{
		itsWidth = itsGUIWidth;
		}
}

/******************************************************************************
 TEGetMinPreferredGUIWidth

 ******************************************************************************/

JCoordinate
JTextEditor::TEGetMinPreferredGUIWidth()
	const
{
	if (itsBreakCROnlyFlag)
		{
		return itsLeftMarginWidth + itsWidth + kRightMarginWidth;
		}
	else
		{
		return itsLeftMarginWidth + itsMaxWordWidth + kRightMarginWidth;
		}
}

/******************************************************************************
 TESetBoundsWidth (protected)

	Call this to notify us of a change in aperture width.

 ******************************************************************************/

void
JTextEditor::TESetBoundsWidth
	(
	const JCoordinate width
	)
{
	itsGUIWidth = width - itsLeftMarginWidth - kRightMarginWidth;
	assert( itsGUIWidth > 0 );
	TEGUIWidthChanged();
}

/******************************************************************************
 TESetLeftMarginWidth (protected)

	For multi-line input areas it helps to have a wide left margin where
	one can click and drag down to select entire lines.

 ******************************************************************************/

void
JTextEditor::TESetLeftMarginWidth
	(
	const JCoordinate origWidth
	)
{
	const JCoordinate width = JMax((JCoordinate) kMinLeftMarginWidth, origWidth);

	if (width != itsLeftMarginWidth)
		{
		itsGUIWidth       += itsLeftMarginWidth;
		itsLeftMarginWidth = width;
		itsGUIWidth       -= itsLeftMarginWidth;
		TEGUIWidthChanged();
		}
}

/******************************************************************************
 TEGUIWidthChanged (private)

 ******************************************************************************/

void
JTextEditor::TEGUIWidthChanged()
{
	if (itsBreakCROnlyFlag)
		{
		TERefresh();
		}
	else if (itsWidth != itsGUIWidth)
		{
		itsWidth = itsGUIWidth;
		RecalcAll(kFalse);
		}
}

/******************************************************************************
 Get current font

 ******************************************************************************/

const JCharacter*
JTextEditor::GetCurrentFontName()
	const
{
	const Font f = GetCurrentFont();
	return itsFontMgr->GetFontName(f.id);
}

JSize
JTextEditor::GetCurrentFontSize()
	const
{
	const Font f = GetCurrentFont();
	return f.size;
}

JFontStyle
JTextEditor::GetCurrentFontStyle()
	const
{
	const Font f = GetCurrentFont();
	return f.style;
}

void
JTextEditor::GetCurrentFont
	(
	JString*	name,
	JSize*		size,
	JFontStyle*	style
	)
	const
{
	const Font f = GetCurrentFont();
	*name  = itsFontMgr->GetFontName(f.id);
	*size  = f.size;
	*style = f.style;
}

// protected

JTextEditor::Font
JTextEditor::GetCurrentFont()
	const
{
	if (!itsSelection.IsEmpty())
		{
		return itsStyles->GetElement(itsSelection.first);
		}
	else
		{
		return itsInsertionFont;
		}
}

/******************************************************************************
 Set current font

 ******************************************************************************/

void
JTextEditor::SetCurrentFontName
	(
	const JCharacter* name
	)
{
	if (!itsSelection.IsEmpty())
		{
		JBoolean isNew;
		JTEUndoStyle* undo = GetStyleUndo(&isNew);

		const JBoolean changed =
			SetFontName(itsSelection.first, itsSelection.last, name, kFalse);

		if (changed && isNew)
			{
			NewUndo(undo);
			}
		else if (isNew)
			{
			delete undo;
			}
		}
	else
		{
		itsInsertionFont.id =
			itsFontMgr->GetFontID(name, itsInsertionFont.size, itsInsertionFont.style);
		}
}

void
JTextEditor::SetCurrentFontSize
	(
	const JSize size
	)
{
	#define LocalVarName      size
	#define StructElementName size
	#include <JTESetFont.th>
	#undef LocalVarName
	#undef StructElementName
}

void
JTextEditor::SetCurrentFontBold
	(
	const JBoolean bold
	)
{
	#define LocalVarName      bold
	#define StructElementName style.bold
	#include <JTESetFont.th>
	#undef LocalVarName
	#undef StructElementName
}

void
JTextEditor::SetCurrentFontItalic
	(
	const JBoolean italic
	)
{
	#define LocalVarName      italic
	#define StructElementName style.italic
	#include <JTESetFont.th>
	#undef LocalVarName
	#undef StructElementName
}

void
JTextEditor::SetCurrentFontUnderline
	(
	const JSize count
	)
{
	#define LocalVarName      count
	#define StructElementName style.underlineCount
	#include <JTESetFont.th>
	#undef LocalVarName
	#undef StructElementName
}

void
JTextEditor::SetCurrentFontStrike
	(
	const JBoolean strike
	)
{
	#define LocalVarName      strike
	#define StructElementName style.strike
	#include <JTESetFont.th>
	#undef LocalVarName
	#undef StructElementName
}

void
JTextEditor::SetCurrentFontColor
	(
	const JColorIndex color
	)
{
	#define LocalVarName      color
	#define StructElementName style.color
	#include <JTESetFont.th>
	#undef LocalVarName
	#undef StructElementName
}

void
JTextEditor::SetCurrentFontStyle
	(
	const JFontStyle& style
	)
{
	#define LocalVarName      style
	#define StructElementName style
	#include <JTESetFont.th>
	#undef LocalVarName
	#undef StructElementName
}

void
JTextEditor::SetCurrentFont
	(
	const JCharacter*	name,
	const JSize			size,
	const JFontStyle&	style
	)
{
	SetCurrentFont(Font(itsFontMgr->GetFontID(name, size, style), size, style));
}

// protected

void
JTextEditor::SetCurrentFont
	(
	const Font& f
	)
{
	if (!itsSelection.IsEmpty())
		{
		JBoolean isNew;
		JTEUndoStyle* undo = GetStyleUndo(&isNew);

		SetFont(itsSelection.first, itsSelection.last, f, kFalse);

		if (isNew)
			{
			NewUndo(undo);
			}
		}
	else
		{
		itsInsertionFont = f;
		}
}

/******************************************************************************
 Get font

 ******************************************************************************/

const JCharacter*
JTextEditor::GetFontName
	(
	const JIndex charIndex
	)
	const
{
	const Font f = itsStyles->GetElement(charIndex);
	return itsFontMgr->GetFontName(f.id);
}

JSize
JTextEditor::GetFontSize
	(
	const JIndex charIndex
	)
	const
{
	const Font f = itsStyles->GetElement(charIndex);
	return f.size;
}

JFontStyle
JTextEditor::GetFontStyle
	(
	const JIndex charIndex
	)
	const
{
	const Font f = itsStyles->GetElement(charIndex);
	return f.style;
}

void
JTextEditor::GetFont
	(
	const JIndex	charIndex,
	JString*		name,
	JSize*			size,
	JFontStyle*		style
	)
	const
{
	const Font f = itsStyles->GetElement(charIndex);
	*name  = itsFontMgr->GetFontName(f.id);
	*size  = f.size;
	*style = f.style;
}

/******************************************************************************
 Set font

	Returns kTrue if anything actually changed

 ******************************************************************************/

JBoolean
JTextEditor::SetFontName
	(
	const JIndex		startIndex,
	const JIndex		endIndex,
	const JCharacter*	name,
	const JBoolean		clearUndo
	)
{
	if (clearUndo)
		{
		ClearUndo();
		}

	Font f;
	JBoolean changed = kFalse;
	JRunArrayIterator<Font> iter(itsStyles, kJIteratorStartBefore, startIndex);
	for (JIndex i=startIndex; i<=endIndex; i++)
		{
		const JBoolean ok = iter.Next(&f);
		assert( ok );

		const JFontID newID = itsFontMgr->GetFontID(name, f.size, f.style);
		if (newID != f.id)
			{
			f.id = newID;
			iter.SetPrev(f);
			changed = kTrue;
			}
		}

	if (changed)
		{
		Recalc(startIndex, endIndex - startIndex + 1, kFalse);
		}

	return changed;
}

JBoolean
JTextEditor::SetFontSize
	(
	const JIndex	startIndex,
	const JIndex	endIndex,
	const JSize		size,
	const JBoolean	clearUndo
	)
{
	if (clearUndo)
		{
		ClearUndo();
		}

	Font f;
	JBoolean changed = kFalse;
	JRunArrayIterator<Font> iter(itsStyles, kJIteratorStartBefore, startIndex);
	for (JIndex i=startIndex; i<=endIndex; i++)
		{
		const JBoolean ok = iter.Next(&f);
		assert( ok );

		if (size != f.size)
			{
			f.size = size;

			f.id = itsFontMgr->UpdateFontID(f.id, f.size, f.style);
			iter.SetPrev(f);
			changed = kTrue;
			}
		}

	if (changed)
		{
		Recalc(startIndex, endIndex - startIndex + 1, kFalse);
		}

	return changed;
}

JBoolean
JTextEditor::SetFontStyle
	(
	const JIndex		startIndex,
	const JIndex		endIndex,
	const JFontStyle&	style,
	const JBoolean		clearUndo
	)
{
	if (clearUndo)
		{
		ClearUndo();
		}

	Font f;
	JBoolean changed = kFalse;
	JRunArrayIterator<Font> iter(itsStyles, kJIteratorStartBefore, startIndex);
	for (JIndex i=startIndex; i<=endIndex; i++)
		{
		const JBoolean ok = iter.Next(&f);
		assert( ok );

		if (style != f.style)
			{
			f.style = style;

			f.id = itsFontMgr->UpdateFontID(f.id, f.size, f.style);
			iter.SetPrev(f);
			changed = kTrue;
			}
		}

	if (changed)
		{
		Recalc(startIndex, endIndex - startIndex + 1, kFalse);
		}

	return changed;
}

void
JTextEditor::SetFont
	(
	const JIndex		startIndex,
	const JIndex		endIndex,
	const JCharacter*	name,
	const JSize			size,
	const JFontStyle&	style,
	const JBoolean		clearUndo
	)
{
	SetFont(startIndex, endIndex,
			Font(itsFontMgr->GetFontID(name, size, style), size, style), clearUndo);
}

// protected

void
JTextEditor::SetFont
	(
	const JIndex		startIndex,
	const JIndex		endIndex,
	const Font&			f,
	const JBoolean		clearUndo
	)
{
	if (clearUndo)
		{
		ClearUndo();
		}

	if (endIndex > startIndex)
		{
		const JSize charCount = endIndex - startIndex + 1;
		itsStyles->SetNextElements(startIndex, charCount, f);
		Recalc(startIndex, charCount, kFalse);
		}
	else
		{
		assert( startIndex == endIndex );

		itsStyles->SetElement(startIndex, f);
		Recalc(startIndex, 1, kFalse);
		}
}

// protected

void
JTextEditor::SetFont
	(
	const JIndex			startIndex,
	const JRunArray<Font>&	fontList,
	const JBoolean			clearUndo
	)
{
	if (clearUndo)
		{
		ClearUndo();
		}

	Font f;
	JRunArrayIterator<Font> fIter(fontList);
	JRunArrayIterator<Font> sIter(itsStyles, kJIteratorStartBefore, startIndex);

	while (fIter.Next(&f) && sIter.SetNext(f))
		{
		sIter.SkipNext();
		}

	Recalc(startIndex, fontList.GetElementCount(), kFalse);
}

/******************************************************************************
 SetAllFontNameAndSize (protected)

	This function is useful for unstyled text editors that allow the user
	to change the font and size.

	It preserves the styles, in case they are controlled by the program.
	(e.g. context sensitive hilighting)

	You can choose whether or not to throw out all Undo information.
	Unstyled text editors can usually preserve Undo, since they will not
	allow the user to modify styles.  (We explicitly ask for this because
	it is too easy to forget about the issue.)

 ******************************************************************************/

void
JTextEditor::SetAllFontNameAndSize
	(
	const JCharacter*	name,
	const JSize			size,
	const JCoordinate	tabWidth,
	const JBoolean		breakCROnly,
	const JBoolean		clearUndo
	)
{
	if (clearUndo)
		{
		ClearUndo();
		}

	const JSize runCount = itsStyles->GetRunCount();
	for (JIndex i=1; i<=runCount; i++)
		{
		Font f = itsStyles->GetRunData(i);
		f.size = size;
		f.id   = itsFontMgr->GetFontID(name, f.size, f.style);
		itsStyles->SetRunData(i, f);
		}

	if (itsUndoList != NULL)
		{
		const JSize undoCount = itsUndoList->GetElementCount();
		for (JIndex i=1; i<=undoCount; i++)
			{
			(itsUndoList->NthElement(i))->SetFont(name, size);
			}
		}
	else if (itsUndo != NULL)
		{
		itsUndo->SetFont(name, size);
		}

	itsInsertionFont.size = size;
	itsInsertionFont.id =
		itsFontMgr->GetFontID(name, itsInsertionFont.size, itsInsertionFont.style);

	SetDefaultFont(name, size, itsDefFont.style);

	itsDefTabWidth = tabWidth;
	PrivateSetBreakCROnly(breakCROnly);
	RecalcAll(kFalse);
}

/******************************************************************************
 Get default font

 ******************************************************************************/

const JCharacter*
JTextEditor::GetDefaultFontName()
	const
{
	return itsFontMgr->GetFontName(itsDefFont.id);
}

void
JTextEditor::GetDefaultFont
	(
	JString*	name,
	JSize*		size,
	JFontStyle*	style
	)
	const
{
	*name  = GetDefaultFontName();
	*size  = itsDefFont.size;
	*style = itsDefFont.style;
}

/******************************************************************************
 Set default font

 ******************************************************************************/

void
JTextEditor::SetDefaultFontName
	(
	const JCharacter* name
	)
{
	itsDefFont.id = itsFontMgr->GetFontID(name, itsDefFont.size, itsDefFont.style);
	if (itsBuffer->IsEmpty())
		{
		itsInsertionFont = CalcInsertionFont(1);
		}
}

void
JTextEditor::SetDefaultFontSize
	(
	const JSize size
	)
{
	itsDefFont.size = size;
	itsDefFont.id   =
		itsFontMgr->UpdateFontID(itsDefFont.id, itsDefFont.size, itsDefFont.style);
	if (itsBuffer->IsEmpty())
		{
		itsInsertionFont = CalcInsertionFont(1);
		}
}

void
JTextEditor::SetDefaultFontStyle
	(
	const JFontStyle& style
	)
{
	itsDefFont.style = style;
	itsDefFont.id    =
		itsFontMgr->UpdateFontID(itsDefFont.id, itsDefFont.size, itsDefFont.style);
	if (itsBuffer->IsEmpty())
		{
		itsInsertionFont = CalcInsertionFont(1);
		}
}

void
JTextEditor::SetDefaultFont
	(
	const JFontID		id,
	const JSize			size,
	const JFontStyle&	style
	)
{
	itsDefFont.id    = id;
	itsDefFont.size  = size;
	itsDefFont.style = style;

	if (itsBuffer->IsEmpty())
		{
		itsInsertionFont = CalcInsertionFont(1);
		}
}

/******************************************************************************
 Cut

 ******************************************************************************/

void
JTextEditor::Cut()
{
	TECreateClipboard();

	if (Cut(itsClipText, itsClipStyle))
		{
		TEClipboardChanged();
		}
}

/******************************************************************************
 Cut

	Returns kTrue if there was anything to cut.  style can be NULL.
	If function returns kFalse, text and style are not modified.

 ******************************************************************************/

JBoolean
JTextEditor::Cut
	(
	JString*			text,
	JRunArray<Font>*	style
	)
{
	if (Copy(text, style))
		{
		DeleteSelection();
		DeactivateCurrentUndo();
		return kTrue;
		}
	else
		{
		return kFalse;
		}
}

/******************************************************************************
 Copy

 ******************************************************************************/

void
JTextEditor::Copy()
{
	TECreateClipboard();

	if (Copy(itsClipText, itsClipStyle))
		{
		TEClipboardChanged();
		}
}

/******************************************************************************
 Copy

	Returns kTrue if there was anything to copy.  style can be NULL.
	If function returns kFalse, text and style are not modified, so
	we cannot call GetSelection().

 ******************************************************************************/

JBoolean
JTextEditor::Copy
	(
	JString*			text,
	JRunArray<Font>*	style
	)
	const
{
	if (!itsSelection.IsEmpty())
		{
		*text = itsBuffer->GetSubstring(itsSelection);
		style->RemoveAll();
		style->InsertElementsAtIndex(1, *itsStyles, itsSelection.first,
									 itsSelection.GetLength());
		return kTrue;
		}
	else
		{
		return kFalse;
		}
}

/******************************************************************************
 GetClipboard

	Returns the text and style that would be pasted if Paste() were called.

	style can be NULL.  If it is not NULL, it can come back empty even if
	the function returns kTrue.

 ******************************************************************************/

JBoolean
JTextEditor::GetClipboard
	(
	JString*			text,
	JRunArray<Font>*	style
	)
	const
{
	if (TEOwnsClipboard() && itsClipText != NULL)
		{
		*text = *itsClipText;
		if (style != NULL)
			{
			*style = *itsClipStyle;
			}
		return kTrue;
		}
	else if (style != NULL)
		{
		return TEGetExternalClipboard(text, style);
		}
	else
		{
		JRunArray<Font> tempStyle;
		return TEGetExternalClipboard(text, &tempStyle);
		}
}

/******************************************************************************
 Paste

 ******************************************************************************/

void
JTextEditor::Paste()
{
	JString text;
	JRunArray<Font> style;
	if (TEOwnsClipboard() && itsClipText != NULL)
		{
		Paste(*itsClipText, itsClipStyle);
		}
	else if (TEGetExternalClipboard(&text, &style))
		{
		if (style.IsEmpty())
			{
			Paste(text);
			}
		else
			{
			Paste(text, &style);
			}
		}
}

/******************************************************************************
 Paste

	style can be NULL

 ******************************************************************************/

void
JTextEditor::Paste
	(
	const JCharacter*		text,
	const JRunArray<Font>*	style
	)
{
	JSize pasteLength;
	if (style != NULL)
		{
		pasteLength = style->GetElementCount();
		}
	else
		{
		pasteLength = strlen(text);
		}

	JTEUndoPaste* newUndo = NULL;
	if (ContainsIllegalChars(text, pasteLength))
		{
		JString newText(text, pasteLength);
		JRunArray<Font>* newStyle = NULL;
		if (style != NULL)
			{
			newStyle = new JRunArray<Font>(*style);
			assert( newStyle != NULL );
			}
		RemoveIllegalChars(&newText, newStyle);

		newUndo = new JTEUndoPaste(this, newText.GetLength());
		PrivatePaste(newText, newStyle);

		delete newStyle;
		}
	else
		{
		newUndo = new JTEUndoPaste(this, pasteLength);
		PrivatePaste(text, style);
		}

	assert( newUndo != NULL );
	NewUndo(newUndo);
}

/******************************************************************************
 PrivatePaste (private)

	style can be NULL

 ******************************************************************************/

void
JTextEditor::PrivatePaste
	(
	const JCharacter*		text,
	const JRunArray<Font>*	style
	)
{
	const JBoolean hadSelection = JNegate( itsSelection.IsEmpty() );
	if (hadSelection)
		{
		itsInsertionFont = itsStyles->GetElement(itsSelection.first);
		DeleteText(itsSelection);
		itsCaretLoc = CalcCaretLocation(itsSelection.first);
		itsSelection.SetToNothing();
		}

	if (itsPasteStyledTextFlag)
		{
		InsertText(itsCaretLoc.charIndex, text, style);
		}
	else
		{
		InsertText(itsCaretLoc.charIndex, text);
		}

	const JSize textLen = strlen(text);
	Recalc(itsCaretLoc, textLen, hadSelection);
	SetCaretLocation(itsCaretLoc.charIndex + textLen);
}

/******************************************************************************
 GetInternalClipboard (protected)

	Returns kTrue if our internal clipboard contains something.

	style can be NULL.

 ******************************************************************************/

JBoolean
JTextEditor::GetInternalClipboard
	(
	const JString**			text,
	const JRunArray<Font>**	style
	)
	const
{
	if (itsClipText != NULL && itsClipStyle != NULL)
		{
		*text = itsClipText;
		if (style != NULL)
			{
			*style = itsClipStyle;
			}
		return kTrue;
		}
	else
		{
		*text = NULL;
		if (style != NULL)
			{
			*style = NULL;
			}
		return kFalse;
		}
}

/******************************************************************************
 TECreateClipboard (private)

	Allocate itsClipText and itsClipStyle.

 ******************************************************************************/

void
JTextEditor::TECreateClipboard()
{
	assert( (itsClipText == NULL && itsClipStyle == NULL) ||
			(itsClipText != NULL && itsClipStyle != NULL) );

	if (itsClipText == NULL)
		{
		itsClipText = new JString;
		assert( itsClipText != NULL );

		itsClipStyle = new JRunArray<Font>;
		assert( itsClipStyle != NULL );
		}
}

/******************************************************************************
 TEClearClipboard (protected)

	Delete itsClipText and itsClipStyle.

 ******************************************************************************/

void
JTextEditor::TEClearClipboard()
{
	delete itsClipText;
	itsClipText = NULL;

	delete itsClipStyle;
	itsClipStyle = NULL;
}

/******************************************************************************
 GetInsertionIndex

	Return the index where new text will be typed or pasted.

 ******************************************************************************/

JIndex
JTextEditor::GetInsertionIndex()
	const
{
	if (!itsSelection.IsEmpty())
		{
		return itsSelection.first;
		}
	else
		{
		return itsCaretLoc.charIndex;
		}
}

/******************************************************************************
 GetSelection

 ******************************************************************************/

JBoolean
JTextEditor::GetSelection
	(
	JString* text
	)
	const
{
	if (!itsSelection.IsEmpty())
		{
		*text = itsBuffer->GetSubstring(itsSelection);
		return kTrue;
		}
	else
		{
		text->Clear();
		return kFalse;
		}
}

JBoolean
JTextEditor::GetSelection
	(
	JString*			text,
	JRunArray<Font>*	style
	)
	const
{
	style->RemoveAll();

	if (!itsSelection.IsEmpty())
		{
		*text = itsBuffer->GetSubstring(itsSelection);
		style->InsertElementsAtIndex(1, *itsStyles, itsSelection.first,
									 itsSelection.GetLength());
		return kTrue;
		}
	else
		{
		text->Clear();
		return kFalse;
		}
}

/******************************************************************************
 SetSelection

 ******************************************************************************/

void
JTextEditor::SetSelection
	(
	const JIndex startIndex,
	const JIndex endIndex
	)
{
	DeactivateCurrentUndo();
	itsPrevDragType = kInvalidDrag;		// avoid wordSel and lineSel pivots

	if (itsBuffer->IsEmpty() || itsSelection.Is(startIndex, endIndex))
		{
		return;
		}

	assert( IndexValid(startIndex) );
	assert( IndexValid(endIndex) );
	assert( startIndex <= endIndex );

	const JBoolean hadSelection      = JNegate( itsSelection.IsEmpty() );
	const CaretLocation origCaretLoc = itsCaretLoc;
	const JIndexRange origSelection  = itsSelection;

	itsCaretLoc = CaretLocation(0,0);
	itsSelection.Set(startIndex, endIndex);

	const JIndex newStartLine = GetLineForChar(itsSelection.first);
	const JIndex newEndLine   = GetLineForChar(itsSelection.last);

	Broadcast(CaretLineChanged(newStartLine));

	TECaretShouldBlink(kFalse);

	if (itsCopyWhenSelectFlag)
		{
		Copy();
		}

	// We only optimize heavily for the case when one end of the
	// selection remains fixed because this is the case during mouse drags.

	if (hadSelection && origSelection.first == itsSelection.first)
		{
		const JIndex origEndLine = GetLineForChar(origSelection.last);
		TERefreshLines(JMin(origEndLine, newEndLine),
					   JMax(origEndLine, newEndLine));
		}
	else if (hadSelection && origSelection.last == itsSelection.last)
		{
		const JIndex origStartLine = GetLineForChar(origSelection.first);
		TERefreshLines(JMin(origStartLine, newStartLine),
					   JMax(origStartLine, newStartLine));
		}
	else if (hadSelection)
		{
		const JIndex origStartLine = GetLineForChar(origSelection.first);
		const JIndex origEndLine   = GetLineForChar(origSelection.last);
		TERefreshLines(origStartLine, origEndLine);
		TERefreshLines(newStartLine, newEndLine);
		}
	else
		{
		TERefreshCaret(origCaretLoc);
		TERefreshLines(newStartLine, newEndLine);
		}
}

/******************************************************************************
 SelectAll

 ******************************************************************************/

void
JTextEditor::SelectAll()
{
	if (!itsBuffer->IsEmpty())
		{
		SetSelection(1, itsBuffer->GetLength());
		}
}

/******************************************************************************
 DeleteSelection

	We create JTEUndoTyping so keys pressed after the delete key count
	as part of the undo task.

 ******************************************************************************/

void
JTextEditor::DeleteSelection()
{
	if (!itsSelection.IsEmpty())
		{
		JTEUndoTyping* newUndo = new JTEUndoTyping(this);
		assert( newUndo != NULL );

		DeleteText(itsSelection);
		Recalc(itsSelection.first, 1, kTrue);
		SetCaretLocation(itsSelection.first);

		NewUndo(newUndo);
		}
}

/******************************************************************************
 DeleteToStartOfWord

	Delete characters until start-of-word is reached.

 ******************************************************************************/

void
JTextEditor::DeleteToStartOfWord()
{
	if (itsCaretLoc.charIndex > 1)
		{
		if (itsSelection.IsEmpty())
			{
			const JIndex startIndex = GetWordStart(itsCaretLoc.charIndex-1);
			SetSelection(startIndex, itsCaretLoc.charIndex-1);
			}

		DeleteSelection();
		}
}

/******************************************************************************
 DeleteToEndOfWord

	Delete characters until end-of-word is reached.

 ******************************************************************************/

void
JTextEditor::DeleteToEndOfWord()
{
	if (itsCaretLoc.charIndex <= itsBuffer->GetLength())
		{
		if (itsSelection.IsEmpty())
			{
			const JIndex endIndex = GetWordEnd(itsCaretLoc.charIndex);
			SetSelection(itsCaretLoc.charIndex, endIndex);
			}

		DeleteSelection();
		}
}

/******************************************************************************
 TabSelectionLeft

	Remove tabs from the beginning of every selected line.  If nothing
	is selected, we select the line that the caret is on.

 ******************************************************************************/

void
JTextEditor::TabSelectionLeft
	(
	const JSize tabCount
	)
{
JIndex i,j;

	JIndex firstLine, lastLine;
	if (!itsSelection.IsEmpty())
		{
		firstLine = GetLineForChar(GetParagraphStart(itsSelection.first));
		lastLine  = GetLineForChar(GetParagraphEnd(itsSelection.last));
		}
	else if (!itsBuffer->IsEmpty())
		{
		firstLine = GetLineForChar(GetParagraphStart(itsCaretLoc.charIndex));
		lastLine  = GetLineForChar(GetParagraphEnd(itsCaretLoc.charIndex));
		}
	else
		{
		return;
		}

	// check that there are enough tabs at the start of every selected line,
	// ignoring lines created by wrapping

	for (i=firstLine; i<=lastLine; i++)
		{
		const JIndex firstChar = GetLineStart(i);
		if (firstChar == 1 || itsBuffer->GetCharacter(firstChar-1) == '\n')
			{
			for (j=firstChar; j<firstChar+tabCount; j++)
				{
				const JCharacter c = itsBuffer->GetCharacter(j);
				if (c != '\t' && c != '\n')
					{
					return;
					}
				if (c == '\n')
					{
					break;	// line is blank or contains only tabs
					}
				}
			}
		}

	JBoolean isNew;
	JTEUndoTabShift* undo = GetTabShiftUndo(firstLine, &isNew);
	undo->HandleShiftLeft(tabCount);

	itsSelection.SetToNothing();
	JSize deleteCount = 0;
	for (i=firstLine; i<=lastLine; i++)
		{
		const JIndex charIndex = GetLineStart(i) - deleteCount;
		if (charIndex == 1 || itsBuffer->GetCharacter(charIndex-1) == '\n')
			{
			for (j=1; j<=tabCount; j++)
				{
				// The deletion point stays in same place (charIndex) and
				// simply eats characters.

				if (itsBuffer->GetCharacter(charIndex) != '\t')
					{
					break;
					}
				DeleteText(charIndex, charIndex);
				deleteCount++;
				}
			}
		}

	const JIndex startIndex = GetLineStart(firstLine);
	const JIndex endIndex   = GetLineEnd(lastLine);
	Recalc(startIndex, endIndex - startIndex + 1, kTrue);

	SetSelection(startIndex, endIndex - deleteCount);
	undo->UpdateEndChar();

	if (isNew)
		{
		NewUndo(undo);
		}
}

/******************************************************************************
 TabSelectionRight

	Insert a tab at the beginning of every selected line.  If nothing
	is selected, we select the line that the caret is on.

 ******************************************************************************/

void
JTextEditor::TabSelectionRight
	(
	const JSize tabCount
	)
{
JIndex i;

	JIndex firstLine, lastLine;
	if (!itsSelection.IsEmpty())
		{
		firstLine = GetLineForChar(GetParagraphStart(itsSelection.first));
		lastLine  = GetLineForChar(GetParagraphEnd(itsSelection.last));
		}
	else if (!itsBuffer->IsEmpty())
		{
		firstLine = GetLineForChar(GetParagraphStart(itsCaretLoc.charIndex));
		lastLine  = GetLineForChar(GetParagraphEnd(itsCaretLoc.charIndex));
		}
	else
		{
		return;
		}

	JBoolean isNew;
	JTEUndoTabShift* undo = GetTabShiftUndo(firstLine, &isNew);
	undo->HandleShiftRight(tabCount);

	JString tabs;
	for (i=1; i<=tabCount; i++)
		{
		tabs.AppendCharacter('\t');
		}

	itsSelection.SetToNothing();
	JSize insertCount = 0;
	for (i=firstLine; i<=lastLine; i++)
		{
		const JIndex charIndex = GetLineStart(i) + insertCount;
		if ((charIndex == 1 || itsBuffer->GetCharacter(charIndex-1) == '\n') &&
			itsBuffer->GetCharacter(charIndex) != '\n')
			{
			InsertText(charIndex, tabs);
			insertCount += tabCount;
			}
		}

	const JIndex startIndex = GetLineStart(firstLine);
	const JIndex endIndex   = GetLineEnd(lastLine) + insertCount;
	Recalc(startIndex, endIndex - startIndex + 1, kFalse);

	SetSelection(startIndex, endIndex);
	undo->UpdateEndChar();

	if (isNew)
		{
		NewUndo(undo);
		}
}

/******************************************************************************
 CleanRightMargin

	If there is a selection, cleans up the right margin of each paragraph
	touched by the selection.  Otherwise, cleans up the right margin of the
	paragraph containing the caret and maintains the caret position.

	If coerce, paragraphs are detected by looking only for blank lines.
	Otherwise, they are detected by blank lines or a change in the line prefix.

	If anything is changed, returns kTrue and sets *reformatRange to the
	range of reformatted text, excluding the last newline.

	The work done by this function can be changed by calling SetCRMRuleList()
	and overriding the virtual CRM*() functions.

 ******************************************************************************/

JBoolean
JTextEditor::CleanRightMargin
	(
	const JBoolean	coerce,
	JIndexRange*	reformatRange
	)
{
	JBoolean changed = kFalse;
	JIndexRange origTextRange;
	JString newText;
	JRunArray<Font> newStyles;
	JIndex newCaretIndex;
	if (itsSelection.IsEmpty())
		{
		changed = PrivateCleanRightMargin(coerce, &origTextRange, &newText,
										  &newStyles, &newCaretIndex);
		}
	else
		{
		const JIndex endChar = GetParagraphEnd(itsSelection.last) - 1;

		JIndexRange range;
		JString text;
		JRunArray<Font> styles;
		JIndex caretIndex;
		while (1)
			{
			if (PrivateCleanRightMargin(coerce, &range, &text, &styles, &caretIndex))
				{
				origTextRange += range;
				newText       += text;
				newStyles.InsertElementsAtIndex(newStyles.GetElementCount()+1,
												styles, 1, styles.GetElementCount());
				if (range.last < endChar)
					{
					(range.last)++;
					(origTextRange.last)++;
					newText.AppendCharacter('\n');
					newStyles.AppendElement(newStyles.GetLastElement());
					}
				}
			else
				{
				caretIndex = GetInsertionIndex();
				range.Set(GetParagraphStart(caretIndex),
						  GetParagraphEnd(caretIndex));

				assert( newText.IsEmpty() ||
						range.first == origTextRange.last + 1 );

				origTextRange += range;
				newText       += itsBuffer->GetSubstring(range);
				newStyles.InsertElementsAtIndex(newStyles.GetElementCount()+1,
												*itsStyles, range.first, range.GetLength());
				}

			if (range.last < endChar && IndexValid(range.last + 1))
				{
				SetCaretLocation(range.last + 1);
				}
			else
				{
				break;
				}
			}

		newCaretIndex = origTextRange.first;
		changed       = kTrue;
		}

	if (changed)
		{
		SetSelection(origTextRange);
		Paste(newText, &newStyles);
		SetCaretLocation(newCaretIndex);

		reformatRange->SetFirstAndLength(origTextRange.first, newText.GetLength());
		return kTrue;
		}
	else
		{
		reformatRange->SetToNothing();
		return kFalse;
		}
}

/******************************************************************************
 PrivateCleanRightMargin (private)

	*newText is the cleaned up version of the paragraph containing the caret
	or start of selection.

	*origTextRange contains the range that was cleaned up, excluding the last
	newline.

	*newCaretIndex is the index required to maintain the position of the caret.

	Returns kFalse if the caret was not in a paragraph.

 ******************************************************************************/

JBoolean
JTextEditor::PrivateCleanRightMargin
	(
	const JBoolean		coerce,
	JIndexRange*		origTextRange,
	JString*			newText,
	JRunArray<Font>*	newStyles,
	JIndex*				newCaretIndex
	)
	const
{
	origTextRange->SetToNothing();
	newText->Clear();
	newStyles->RemoveAll();
	*newCaretIndex = 0;

	if (itsBuffer->IsEmpty())
		{
		return kFalse;
		}

	const JIndex caretChar = GetInsertionIndex();
	if (caretChar == itsBuffer->GetLength()+1 &&		// ends with newline
		EndsWithNewline())
		{
		return kFalse;
		}

	JIndex charIndex, ruleIndex;
	JString firstLinePrefix, restLinePrefix;
	JSize firstPrefixLength, restPrefixLength;
	if (!CRMGetRange(caretChar, coerce, origTextRange, &charIndex,
					 &firstLinePrefix, &firstPrefixLength,
					 &restLinePrefix, &restPrefixLength, &ruleIndex))
		{
		return kFalse;
		}

	if (caretChar <= charIndex)
		{
		*newCaretIndex = caretChar;
		}

	// read in each word, convert it, write it out

	JSize currentLineWidth = 0;
	JBoolean requireSpace  = kFalse;

	JString wordBuffer, spaceBuffer;
	JRunArray<Font> wordStyles;
	while (charIndex <= origTextRange->last)
		{
		JSize spaceCount;
		JIndex rnwCaretIndex = 0;
		const CRMStatus status =
			CRMReadNextWord(&charIndex, origTextRange->last,
							&spaceBuffer, &spaceCount, &wordBuffer, &wordStyles,
							currentLineWidth, caretChar, &rnwCaretIndex,
							*newText, requireSpace);
		requireSpace = kTrue;

		if (status == kFinished)
			{
			assert( charIndex == origTextRange->last+1 );
			break;
			}
		else if (status == kFoundWord)
			{
			if (newText->IsEmpty())
				{
				CRMAppendWord(newText, newStyles, &currentLineWidth, &rnwCaretIndex,
							  spaceBuffer, spaceCount, wordBuffer, wordStyles,
							  firstLinePrefix, firstPrefixLength);
				}
			else
				{
				CRMAppendWord(newText, newStyles, &currentLineWidth, &rnwCaretIndex,
							  spaceBuffer, spaceCount, wordBuffer, wordStyles,
							  restLinePrefix, restPrefixLength);
				}

			if (rnwCaretIndex > 0)
				{
				*newCaretIndex = origTextRange->first + rnwCaretIndex - 1;
				}
			}
		else
			{
			assert( status == kFoundNewLine );

			CRMTossLinePrefix(&charIndex, origTextRange->last, ruleIndex);

			// CRMGetRange() insures this is strictly *inside* the text,
			// so caretChar == charIndex will be caught elsewhere.

			if (*newCaretIndex == 0 && caretChar < charIndex)
				{
				*newCaretIndex = origTextRange->first + newText->GetLength();
				}
			}
		}

	if (caretChar == origTextRange->last+1)
		{
		*newCaretIndex = origTextRange->first + newText->GetLength();
		}

	assert( *newCaretIndex != 0 );
	assert( newText->GetLength() == newStyles->GetElementCount() );

	return kTrue;
}

/*******************************************************************************
 CRMGetRange (private)

	Returns the range of characters to reformat.
	Returns kFalse if the caret is not in a paragraph.

	caretChar is the current location of the caret.

	If coerce, we search forward and backward from the caret location
	for blank lines and include all the text between these blank lines.
	Blank lines are defined as lines that contain nothing but prefix characters.

	Otherwise, we first search backward until we find a blank line or a
	line prefix that can't generate the following one.  Then we search
	forward until we find a blank line or the prefix changes.

	The latter method is safer because it doesn't change the prefix of any of
	the lines.  The former is useful for forcing a change in the prefix.

 ******************************************************************************/

JBoolean
JTextEditor::CRMGetRange
	(
	const JIndex	caretChar,
	const JBoolean	coerce,
	JIndexRange*	range,
	JIndex*			textStartIndex,
	JString*		firstLinePrefix,
	JSize*			firstPrefixLength,
	JString*		restLinePrefix,
	JSize*			restPrefixLength,
	JIndex*			returnRuleIndex
	)
	const
{
	range->Set(GetParagraphStart(caretChar), GetParagraphEnd(caretChar));
	while (range->last > 0 && itsBuffer->GetCharacter(range->last) == '\n')
		{
		range->last--;
		}

	// If the line we are on is empty, quit immediately.

	JIndex tempStart = range->first;
	JString origLinePrefix;
	JSize prefixLength;
	JIndex ruleIndex = 0;
	if (range->IsEmpty() ||
		!CRMGetPrefix(&tempStart, range->last,
					  &origLinePrefix, &prefixLength, &ruleIndex) ||
		CRMLineMatchesRest(*range))
		{
		return kFalse;
		}

	// search backward for a blank line or a change in the prefix (if !coerce)
	// (If range->first==2, the line above us is blank.)

	JString currLinePrefix, nextLinePrefix = origLinePrefix;
	while (range->first > 2)
		{
		const JIndex newStart = GetParagraphStart(range->first - 1);
		tempStart             = newStart;
		ruleIndex             = 0;
		if (tempStart >= range->first - 1 ||
			!CRMGetPrefix(&tempStart, range->first - 2,
						  &currLinePrefix, &prefixLength, &ruleIndex) ||
			CRMLineMatchesRest(JIndexRange(newStart, range->first - 2)) ||
			(!coerce &&
			 CRMBuildRestPrefix(currLinePrefix, ruleIndex, &prefixLength) != nextLinePrefix))
			{
			break;
			}
		range->first   = newStart;
		nextLinePrefix = currLinePrefix;
		ruleIndex      = 0;
		}

	// search forward for a blank line or a change in the prefix (if !coerce)
	// (If range->last==bufLength-1, the text ends with a newline.)

	*textStartIndex  = range->first;
	*returnRuleIndex = 0;
	const JBoolean hasText =
		CRMGetPrefix(textStartIndex, range->last,
					 firstLinePrefix, firstPrefixLength, returnRuleIndex);
	assert( hasText );

	*restLinePrefix = CRMBuildRestPrefix(*firstLinePrefix, *returnRuleIndex,
										 restPrefixLength);

	while (range->last < itsBuffer->GetLength()-1)
		{
		tempStart     = range->last + 2;
		JIndex newEnd = GetParagraphEnd(tempStart);
		if (itsBuffer->GetCharacter(newEnd) == '\n')	// could hit end of text instead
			{
			newEnd--;
			}

		JIndex tempRuleIndex = *returnRuleIndex;
		if (newEnd < tempStart ||
			!CRMGetPrefix(&tempStart, newEnd,
						  &currLinePrefix, &prefixLength, &tempRuleIndex) ||
			(!coerce && currLinePrefix != *restLinePrefix))
			{
			break;
			}
		range->last = newEnd;
		}

	return kTrue;
}

/*******************************************************************************
 CRMGetPrefix (private)

	Returns the prefix to be used for each line and updates *startChar to point
	to the first character after the prefix.

	*prefixLength is set to the length of *linePrefix in characters.  This
	can be greater than linePrefix->GetLength() because of tabs.

	Returns kFalse if the entire range qualifies as a prefix.

 ******************************************************************************/

JBoolean
JTextEditor::CRMGetPrefix
	(
	JIndex*			startChar,
	const JIndex	endChar,
	JString*		linePrefix,
	JSize*			prefixLength,
	JIndex*			ruleIndex
	)
	const
{
	const JIndexRange prefixRange =
		CRMMatchPrefix(JIndexRange(*startChar, endChar), ruleIndex);

	*startChar    = prefixRange.last + 1;
	*linePrefix   = itsBuffer->GetSubstring(prefixRange);
	*prefixLength = CRMCalcPrefixLength(linePrefix);
	return JConvertToBoolean(*startChar <= endChar);
}

/*******************************************************************************
 CRMMatchPrefix (private)

	Returns the range of characters that qualifies as a prefix.

	To match a prefix, all CRMRule::first's and [ \t]* are tried.
	The longest match is used.

	If *ruleIndex > 0, CRMRule::rest for the specified rule is tried *first*,
	and if this matches, it preempts everything else.

	When the function returns, *ruleIndex is the CRMRule::first that matched.
	(It remains 0 if the default rule is used, since this doesn't have "rest".)

 ******************************************************************************/

static const JRegex defaultCRMPrefixRegex = "[ \t]*";

JIndexRange
JTextEditor::CRMMatchPrefix
	(
	const JIndexRange&	textRange,
	JIndex*				ruleIndex
	)
	const
{
	JIndexRange matchRange;

	if (itsCRMRuleList != NULL && *ruleIndex > 0)
		{
		const CRMRule rule = itsCRMRuleList->GetElement(*ruleIndex);
		if ((rule.rest)->MatchWithin(*itsBuffer, textRange, &matchRange) &&
			matchRange.first == textRange.first)
			{
			return matchRange;
			}
		}

	JIndexRange range;
	if (itsCRMRuleList != NULL)
		{
		const JSize count = itsCRMRuleList->GetElementCount();
		for (JIndex i=1; i<=count; i++)
			{
			const CRMRule rule = itsCRMRuleList->GetElement(i);
			if ((rule.first)->MatchWithin(*itsBuffer, textRange, &range) &&
				range.first == textRange.first &&
				range.last  >  matchRange.last)
				{
				matchRange = range;
				*ruleIndex = i;
				}
			}
		}

	// check equality of range::last in case prefix is empty

	const JBoolean defMatch =
		defaultCRMPrefixRegex.MatchWithin(*itsBuffer, textRange, &range);
	assert( defMatch && range.first == textRange.first );
	if (range.last >= matchRange.last || itsCRMRuleList == NULL)
		{
		matchRange = range;
		*ruleIndex = 0;
		}

	return matchRange;
}

/*******************************************************************************
 CRMLineMatchesRest (private)

	Returns kTrue if the given range is matched by any "rest" pattern.
	Used at beginning of CRMGetRange as part of check if line is empty.

 ******************************************************************************/

JBoolean
JTextEditor::CRMLineMatchesRest
	(
	const JIndexRange& range
	)
	const
{
	if (itsCRMRuleList != NULL)
		{
		JIndexRange matchRange;
		const JSize count = itsCRMRuleList->GetElementCount();
		for (JIndex i=1; i<=count; i++)
			{
			const CRMRule rule = itsCRMRuleList->GetElement(i);
			if ((rule.rest)->MatchWithin(*itsBuffer, range, &matchRange) &&
				range == matchRange)
				{
				return kTrue;
				}
			}
		}

	return kFalse;
}

/*******************************************************************************
 CRMCalcPrefixLength (private)

	Returns the length of *linePrefix in characters.  This can be greater
	than linePrefix->GetLength() because of tabs.  *linePrefix may be modified.

 ******************************************************************************/

JSize
JTextEditor::CRMCalcPrefixLength
	(
	JString* linePrefix
	)
	const
{
	if (linePrefix->IsEmpty())
		{
		return 0;
		}

	const JString s    = *linePrefix;
	JSize prefixLength = 0;
	linePrefix->Clear();

	const JSize length = s.GetLength();
	for (JIndex i=1; i<=length; i++)
		{
		const JCharacter c = s.GetCharacter(i);
		if (c == '\t')
			{
			CRMConvertTab(linePrefix, &prefixLength, 0);
			}
		else
			{
			linePrefix->AppendCharacter(c);
			prefixLength++;
			}
		}

	return prefixLength;
}

/*******************************************************************************
 CRMBuildRestPrefix (private)

	Returns the line prefix to be used for all lines after the first one.

 ******************************************************************************/

JString
JTextEditor::CRMBuildRestPrefix
	(
	const JString&	firstLinePrefix,
	const JIndex	ruleIndex,
	JSize*			prefixLength
	)
	const
{
	JString s = firstLinePrefix;
	if (itsCRMRuleList != NULL && ruleIndex > 0)
		{
		JArray<JIndexRange> matchList;
		const CRMRule rule   = itsCRMRuleList->GetElement(ruleIndex);
		const JBoolean match = (rule.first)->Match(s, &matchList);
		assert( match &&
				(matchList.GetFirstElement()).first == 1 &&
				(matchList.GetFirstElement()).last  == s.GetLength() );

		JIndexRange newRange;
		(rule.first)->Replace(&s, matchList, &newRange);
		}

	*prefixLength = CRMCalcPrefixLength(&s);
	return s;
}

/*******************************************************************************
 CRMTossLinePrefix (private)

	Increments *charIndex past whatever qualifies as a line prefix.

	The default is to toss all character approved by CRMIsPrefixChar().

 ******************************************************************************/

void
JTextEditor::CRMTossLinePrefix
	(
	JIndex*			charIndex,
	const JIndex	endChar,
	const JIndex	origRuleIndex
	)
	const
{
	JIndex ruleIndex = origRuleIndex;
	const JIndexRange prefixRange =
		CRMMatchPrefix(JIndexRange(*charIndex, endChar), &ruleIndex);
	*charIndex = prefixRange.last + 1;
}

/*******************************************************************************
 CRMConvertTab (virtual protected)

	Appends the tab to *charBuffer and increments *charCount appropriately.
	The default is to physically append the tab character and use
	CRMGetTabWidth() to increment *charCount.  Note that *charCount may be
	longer than charBuffer->GetLength().

	currentLineWidth is the number of characters on the line -excluding-
	what is in *charBuffer.

	Derived classes can override this to, for example, append some spaces
	instead of a tab character.  (How's that for splitting an infinitive?)

 ******************************************************************************/

void
JTextEditor::CRMConvertTab
	(
	JString*	charBuffer,
	JSize*		charCount,
	const JSize	currentLineWidth
	)
	const
{
	charBuffer->AppendCharacter('\t');
	*charCount += CRMGetTabWidth(currentLineWidth + *charCount);
}

/*******************************************************************************
 CRMGetTabWidth (virtual protected)

	Returns the number of spaces to which the tab is equivalent.
	The default is to round up to the nearest multiple of GetCRMTabCharCount().
	The default value for this is 8 since this is what all UNIX programs use.

	textColumn starts at zero at the left margin.

 ******************************************************************************/

JSize
JTextEditor::CRMGetTabWidth
	(
	const JIndex textColumn
	)
	const
{
	return itsCRMTabCharCount - (textColumn % itsCRMTabCharCount);
}

/*******************************************************************************
 CRMIsEOS (private)

 ******************************************************************************/

inline int
JTextEditor::CRMIsEOS
	(
	const JCharacter c
	)
	const
{
	return (c == '.' || c == '?' || c == '!');
}

/*******************************************************************************
 CRMReadNextWord (private)

	Read one block of { spaces + word (non-spaces) } from itsBuffer, starting
	at *charIndex, stopping if we get as far as endIndex.  *spaceBuffer
	contains the whitespace (spaces + tabs) that was found.  *spaceCount is
	the equivalent number of spaces.  *wordBuffer contains the word.
	*charIndex is incremented to point to the next character to read.

	Newlines are treated separately.  If a newline is encountered while reading
	spaces, we throw out the spaces and return kFoundNewLine.  If a newline
	is encountered while reading a word, we leave it for the next time, when
	we immediately return kFoundNewLine.

	When we pass the position origCaretIndex, we set *newCaretIndex to be
	the index into *spaceBuffer+*wordBuffer.  Otherwise, we do not change
	*newCaretIndex.

 ******************************************************************************/

JTextEditor::CRMStatus
JTextEditor::CRMReadNextWord
	(
	JIndex*				charIndex,
	const JIndex		endIndex,
	JString*			spaceBuffer,
	JSize*				spaceCount,
	JString*			wordBuffer,
	JRunArray<Font>*	wordStyles,
	const JSize			currentLineWidth,
	const JIndex		origCaretIndex,
	JIndex*				newCaretIndex,
	const JString&		newText,
	const JBoolean		requireSpace
	)
	const
{
	// read the whitespace

	spaceBuffer->Clear();
	*spaceCount = 0;

	while (*charIndex <= endIndex)
		{
		if (*charIndex == origCaretIndex)
			{
			*newCaretIndex = spaceBuffer->GetLength() + 1;
			}

		const JCharacter c = itsBuffer->GetCharacter(*charIndex);
		if (c == ' ')
			{
			(*charIndex)++;
			spaceBuffer->AppendCharacter(c);
			(*spaceCount)++;
			}
		else if (c == '\t')
			{
			(*charIndex)++;
			CRMConvertTab(spaceBuffer, spaceCount, currentLineWidth);
			}
		else if (c == '\n')			// we can ignore the spaces
			{
			(*charIndex)++;
			spaceBuffer->Clear();
			*spaceCount = 0;
			return kFoundNewLine;
			}
		else						// found beginning of word
			{
			break;
			}
		}

	if (*charIndex == endIndex+1)	// we can ignore the spaces
		{
		return kFinished;
		}

	// read the word

	wordBuffer->Clear();

	const JIndex wordStart = *charIndex;
	while (*charIndex <= endIndex)
		{
		if (*charIndex == origCaretIndex)
			{
			*newCaretIndex = spaceBuffer->GetLength() + wordBuffer->GetLength() + 1;
			}

		const JCharacter c = itsBuffer->GetCharacter(*charIndex);
		if (c == ' ' || c == '\t' || c == '\n')
			{
			break;
			}

		wordBuffer->AppendCharacter(c);
		(*charIndex)++;
		}

	wordStyles->RemoveAll();
	wordStyles->InsertElementsAtIndex(1, *itsStyles, wordStart, wordBuffer->GetLength());

	// After a newline, the whitespace may have been tossed
	// as belonging to the prefix, but we still need some space.
	//
	// Punctuation is assumed to be the end of a sentence if the
	// following word does not start with a lower case letter.

	if (*spaceCount == 0 && requireSpace)
		{
		JCharacter c1 = newText.GetLastCharacter();
		JCharacter c2 = '\0';
		if (newText.GetLength() > 1)
			{
			c2 = newText.GetCharacter(newText.GetLength()-1);
			}
		if ((CRMIsEOS(c1) || (CRMIsEOS(c2) && c1 =='\"')) &&
			!islower(wordBuffer->GetFirstCharacter()))
			{
			*spaceBuffer = "  ";
			}
		else
			{
			*spaceBuffer = " ";
			}

		*spaceCount = spaceBuffer->GetLength();
		if (*newCaretIndex > 0)
			{
			*newCaretIndex += *spaceCount;
			}
		}

	return kFoundWord;
}

/*******************************************************************************
 CRMAppendWord (private)

	Add the spaces and word to new text, maintaining the required line width.

	If *newCaretIndex>0, we convert it from an index in spaceBuffer+wordBuffer
	to an index in newText.

 ******************************************************************************/

void
JTextEditor::CRMAppendWord
	(
	JString*				newText,
	JRunArray<Font>*		newStyles,
	JSize*					currentLineWidth,
	JIndex*					newCaretIndex,
	const JString&			spaceBuffer,
	const JSize				spaceCount,
	const JString&			wordBuffer,
	const JRunArray<Font>&	wordStyles,
	const JString&			linePrefix,
	const JSize				prefixLength
	)
	const
{
	const JSize newLineWidth = *currentLineWidth + spaceCount + wordBuffer.GetLength();
	if (*currentLineWidth == 0 || newLineWidth > itsCRMLineWidth)
		{
		// calculate prefix font

		Font prefixFont  = wordStyles.GetFirstElement();
		prefixFont.style = JFontStyle();
		prefixFont.id    =
			itsFontMgr->UpdateFontID(prefixFont.id, prefixFont.size, prefixFont.style);

		// terminate previous line

		if (!newText->IsEmpty())
			{
			newText->AppendCharacter('\n');
			newStyles->AppendElement(prefixFont);
			}
		if (!linePrefix.IsEmpty())
			{
			*newText += linePrefix;
			newStyles->AppendElements(prefixFont, linePrefix.GetLength());
			}

		// write word

		if (*newCaretIndex > 0)
			{
			if (*newCaretIndex <= spaceCount)
				{
				*newCaretIndex = 1;				// in spaces that we toss
				}
			else
				{
				*newCaretIndex -= spaceCount;	// in word
				}

			*newCaretIndex += newText->GetLength();
			}

		*newText += wordBuffer;
		newStyles->InsertElementsAtIndex(newStyles->GetElementCount()+1,
										 wordStyles, 1, wordStyles.GetElementCount());
		*currentLineWidth = prefixLength + wordBuffer.GetLength();
		}
	else	// newLineWidth <= itsCRMLineWidth
		{
		// append spaces + word at end of line

		if (*newCaretIndex > 0)
			{
			*newCaretIndex += newText->GetLength();
			}

		*newText += spaceBuffer;
		newStyles->AppendElements(CalcWSFont(newStyles->GetLastElement(),
											 wordStyles.GetFirstElement()),
								  spaceBuffer.GetLength());
		*newText += wordBuffer;
		newStyles->InsertElementsAtIndex(newStyles->GetElementCount()+1,
										 wordStyles, 1, wordStyles.GetElementCount());

		if (newLineWidth < itsCRMLineWidth)
			{
			*currentLineWidth = newLineWidth;
			}
		else	// newLineWidth == itsCRMLineWidth
			{
			*currentLineWidth = 0;
			}
		}
}

/******************************************************************************
 SetCRMRuleList

	To find the first line of a paragraph, one searches backward for a blank
	line:  one that matches any CRMRule::first.  The following line is the
	beginning of the paragraph.

	Each following line is part of the paragraph if it is not blank:
	no CRMRule::first matches the entire line, and neither does the
	CRMRule::rest corresponding to the CRMRule::first that matched the first
	line of the paragraph.

	The prefix of the first line is unchanged.  The prefix of the following
	lines is calculated from calling (CRMRule::first)->Replace() on the
	prefix of the first line.

 ******************************************************************************/

void
JTextEditor::SetCRMRuleList
	(
	CRMRuleList*	ruleList,
	const JBoolean	teOwnsRuleList
	)
{
	ClearCRMRuleList();

	if (ruleList != NULL && teOwnsRuleList)
		{
		itsCRMRuleList = new CRMRuleList(*ruleList);
		assert( itsCRMRuleList != NULL );
		itsOwnsCRMRulesFlag = kTrue;
		}
	else
		{
		itsCRMRuleList      = ruleList;
		itsOwnsCRMRulesFlag = kFalse;
		}
}

/******************************************************************************
 ClearCRMRuleList

	With no other rules, the default is to match [ \t]*

 ******************************************************************************/

void
JTextEditor::ClearCRMRuleList()
{
	if (itsOwnsCRMRulesFlag && itsCRMRuleList != NULL)
		{
		itsCRMRuleList->DeleteAll();
		delete itsCRMRuleList;
		}

	itsCRMRuleList      = NULL;
	itsOwnsCRMRulesFlag = kFalse;
}

/******************************************************************************
 CRMRule functions

 ******************************************************************************/

JRegex*
JTextEditor::CRMRule::CreateFirst
	(
	const JCharacter* pattern,
	const JCharacter* replacePattern
	)
{
	JRegex* r = new JRegex(pattern);
	assert( r != NULL );
	const JError err = r->SetReplacePattern(replacePattern);
	assert_ok( err );
	return r;
}

JRegex*
JTextEditor::CRMRule::CreateRest
	(
	const JCharacter* pattern
	)
{
	JRegex* r = new JRegex(pattern);
	assert( r != NULL );
	return r;
}

/******************************************************************************
 CRMRuleList functions

 ******************************************************************************/

JTextEditor::CRMRuleList::CRMRuleList
	(
	const CRMRuleList& source
	)
	:
	JArray<CRMRule>()
{
	const JSize count = source.GetElementCount();
	for (JIndex i=1; i<=count; i++)
		{
		const CRMRule r = source.GetElement(i);
		AppendElement(
			CRMRule((r.first)->GetPattern(), (r.rest)->GetPattern(),
					(r.first)->GetReplacePattern()));
		}
}

void
JTextEditor::CRMRuleList::DeleteAll()
{
	const JSize count = GetElementCount();
	for (JIndex i=1; i<=count; i++)
		{
		CRMRule r = GetElement(i);
		delete r.first;
		delete r.rest;
		}

	RemoveAll();
}

/******************************************************************************
 Undo

 ******************************************************************************/

void
JTextEditor::Undo()
{
	assert( itsUndoState == kIdle );

	JTEUndoBase* undo;
	const JBoolean hasUndo = GetCurrentUndo(&undo);
	if (hasUndo && itsType == kFullEditor)
		{
		itsUndoState = kUndo;
		undo->Deactivate();
		undo->Undo();
		itsUndoState = kIdle;
		}
	else if (hasUndo)
		{
		ClearUndo();
		}
}

/******************************************************************************
 Redo

 ******************************************************************************/

void
JTextEditor::Redo()
{
	assert( itsUndoState == kIdle );

	JTEUndoBase* undo;
	const JBoolean hasUndo = GetCurrentRedo(&undo);
	if (hasUndo && itsType == kFullEditor)
		{
		itsUndoState = kRedo;
		undo->Deactivate();
		undo->Undo();
		itsUndoState = kIdle;
		}
	else if (hasUndo)
		{
		ClearUndo();
		}
}

/******************************************************************************
 DeactivateCurrentUndo

 ******************************************************************************/

void
JTextEditor::DeactivateCurrentUndo()
{
	JTEUndoBase* undo = NULL;
	if (GetCurrentUndo(&undo))
		{
		undo->Deactivate();
		}
}

/******************************************************************************
 ClearUndo

	Avoid calling this whenever possible since it throws away -all-
	undo information.

 ******************************************************************************/

void
JTextEditor::ClearUndo()
{
	delete itsUndo;
	itsUndo = NULL;

	if (itsUndoList != NULL)
		{
		itsUndoList->DeleteAll();
		}
	itsFirstRedoIndex = 1;
}

/******************************************************************************
 UseMultipleUndo

	You probably never need to turn off multiple undo unless you
	are running out of memory.

 ******************************************************************************/

void
JTextEditor::UseMultipleUndo
	(
	const JBoolean doIt
	)
{
	if (doIt && itsUndoList == NULL)
		{
		ClearUndo();

		itsUndoList = new JPtrArray<JTEUndoBase>(itsMaxUndoCount+1);
		assert( itsUndoList != NULL );
		}
	else if (!doIt && itsUndoList != NULL)
		{
		ClearUndo();

		delete itsUndoList;
		itsUndoList = NULL;
		}
}

/******************************************************************************
 SetUndoDepth

 ******************************************************************************/

void
JTextEditor::SetUndoDepth
	(
	const JSize maxUndoCount
	)
{
	assert( maxUndoCount > 0 );

	itsMaxUndoCount = maxUndoCount;
	ClearOutdatedUndo();
}

/******************************************************************************
 GetCurrentUndo (private)

 ******************************************************************************/

JBoolean
JTextEditor::GetCurrentUndo
	(
	JTEUndoBase** undo
	)
	const
{
	if (itsUndoList != NULL && itsFirstRedoIndex > 1)
		{
		*undo = itsUndoList->NthElement(itsFirstRedoIndex - 1);
		return kTrue;
		}
	else if (itsUndoList != NULL)
		{
		return kFalse;
		}
	else
		{
		*undo = itsUndo;
		return JConvertToBoolean( *undo != NULL );
		}
}

/******************************************************************************
 GetCurrentRedo (private)

 ******************************************************************************/

JBoolean
JTextEditor::GetCurrentRedo
	(
	JTEUndoBase** redo
	)
	const
{
	if (itsUndoList != NULL && itsFirstRedoIndex <= itsUndoList->GetElementCount())
		{
		*redo = itsUndoList->NthElement(itsFirstRedoIndex);
		return kTrue;
		}
	else if (itsUndoList != NULL)
		{
		return kFalse;
		}
	else
		{
		*redo = itsUndo;
		return JConvertToBoolean( *redo != NULL );
		}
}

/******************************************************************************
 NewUndo (private)

	Register a new Undo object.

	itsFirstRedoIndex points to the first redo object in itsUndoList.
	1 <= itsFirstRedoIndex <= itsUndoList->GetElementCount()+1
	Minimum => everything is redo
	Maximum => everything is undo

 ******************************************************************************/

void
JTextEditor::NewUndo
	(
	JTEUndoBase* undo
	)
{
	if (itsUndoList != NULL && itsUndoState == kIdle)
		{
		// clear redo objects

		const JSize undoCount = itsUndoList->GetElementCount();
		for (JIndex i=undoCount; i>=itsFirstRedoIndex; i--)
			{
			itsUndoList->DeleteElement(i);
			}

		// save the new object

		itsUndoList->Append(undo);
		itsFirstRedoIndex++;

		ClearOutdatedUndo();
		assert( !itsUndoList->IsEmpty() );
		}

	else if (itsUndoList != NULL && itsUndoState == kUndo)
		{
		assert( itsFirstRedoIndex > 1 );

		itsFirstRedoIndex--;
		JTEUndoBase* oldUndo = itsUndoList->NthElement(itsFirstRedoIndex);
		delete oldUndo;
		itsUndoList->SetElement(itsFirstRedoIndex, undo);

		undo->SetRedo(kTrue);
		undo->Deactivate();
		}

	else if (itsUndoList != NULL && itsUndoState == kRedo)
		{
		assert( itsFirstRedoIndex <= itsUndoList->GetElementCount() );

		JTEUndoBase* oldRedo = itsUndoList->NthElement(itsFirstRedoIndex);
		delete oldRedo;
		itsUndoList->SetElement(itsFirstRedoIndex, undo);
		itsFirstRedoIndex++;

		undo->SetRedo(kFalse);
		undo->Deactivate();
		}

	else
		{
		delete itsUndo;
		itsUndo = undo;
		}

	Broadcast(TextChanged());
}

/******************************************************************************
 SameUndo (private)

	Called by Undo objects to notify us that there was actually nothing
	to do, so the same object can be re-used.

 ******************************************************************************/

void
JTextEditor::SameUndo
	(
	JTEUndoBase* undo
	)
{
	assert( itsUndoState != kIdle );

	if (itsUndoList != NULL && itsUndoState == kUndo)
		{
		assert( itsFirstRedoIndex > 1 );

		itsFirstRedoIndex--;
		assert( undo == itsUndoList->NthElement(itsFirstRedoIndex) );

		undo->SetRedo(kTrue);
		}

	else if (itsUndoList != NULL && itsUndoState == kRedo)
		{
		assert( itsFirstRedoIndex <= itsUndoList->GetElementCount() );

		assert( undo == itsUndoList->NthElement(itsFirstRedoIndex) );
		itsFirstRedoIndex++;

		undo->SetRedo(kFalse);
		}

	else if (itsUndoList == NULL)
		{
		assert( undo == itsUndo );
		}

	undo->Deactivate();

	// nothing changed, so we don't broadcast TextChanged
}

/******************************************************************************
 ReplaceUndo (private)

 ******************************************************************************/

void
JTextEditor::ReplaceUndo
	(
	JTEUndoBase* oldUndo,
	JTEUndoBase* newUndo
	)
{
#ifndef NDEBUG

	assert( itsUndoState != kIdle );

	if (itsUndoList != NULL && itsUndoState == kUndo)
		{
		assert( itsFirstRedoIndex > 1 &&
				oldUndo == itsUndoList->GetElement(itsFirstRedoIndex - 1) );
		}
	else if (itsUndoList != NULL && itsUndoState == kRedo)
		{
		assert( itsFirstRedoIndex <= itsUndoList->GetElementCount() &&
				oldUndo == itsUndoList->GetElement(itsFirstRedoIndex) );
		}
	else
		{
		assert( oldUndo == itsUndo );
		}

#endif

	NewUndo(newUndo);
}

/******************************************************************************
 ClearOutdatedUndo (private)

 ******************************************************************************/

void
JTextEditor::ClearOutdatedUndo()
{
	if (itsUndoList != NULL)
		{
		JSize undoCount = itsUndoList->GetElementCount();
		while (undoCount > itsMaxUndoCount)
			{
			itsUndoList->DeleteElement(1);
			undoCount--;
			itsFirstRedoIndex--;

			// If we have to throw out redo's, we have to toss everything.

			if (itsFirstRedoIndex == 0)
				{
				ClearUndo();
				break;
				}
			}
		}
}

/******************************************************************************
 GetTypingUndo (private)

	Return the active JTEUndoTyping object.  If the current undo object is
	not an active JTEUndoTyping object, we create a new one that is active.

	If we create a new object, *isNew = kTrue, and the caller is required
	to call NewUndo() after changing the text.

 ******************************************************************************/

JTEUndoTyping*
JTextEditor::GetTypingUndo
	(
	JBoolean* isNew
	)
{
	JTEUndoTyping* typingUndo = NULL;

	JTEUndoBase* undo = NULL;
	if (GetCurrentUndo(&undo) &&
		(typingUndo = undo->CastToJTEUndoTyping()) != NULL &&
		typingUndo->IsActive())
		{
		assert( itsSelection.IsEmpty() );
		*isNew = kFalse;
		return typingUndo;
		}
	else
		{
		typingUndo = new JTEUndoTyping(this);
		assert( typingUndo != NULL );

		*isNew = kTrue;
		return typingUndo;
		}
}

/******************************************************************************
 GetStyleUndo (private)

	Return the active JTEUndoStyle object.  If the current undo object is
	not an active JTEUndoStyle object, we create a new one that is active.

	If we create a new object, *isNew = kTrue, and the caller is required
	to call NewUndo() after changing the text.

 ******************************************************************************/

JTEUndoStyle*
JTextEditor::GetStyleUndo
	(
	JBoolean* isNew
	)
{
	JTEUndoStyle* styleUndo = NULL;

	JTEUndoBase* undo = NULL;
	if (GetCurrentUndo(&undo) &&
		(styleUndo = undo->CastToJTEUndoStyle()) != NULL &&
		styleUndo->IsActive())
		{
		assert( !itsSelection.IsEmpty() );
		*isNew = kFalse;
		return styleUndo;
		}
	else
		{
		styleUndo = new JTEUndoStyle(this);
		assert( styleUndo != NULL );

		*isNew = kTrue;
		return styleUndo;
		}
}

/******************************************************************************
 GetTabShiftUndo (private)

	Return the active JTEUndoTabShift object.  If the current undo object is
	not an active JTEUndoTabShift object, we create a new one that is active.

	If we create a new object, *isNew = kTrue, and the caller is required
	to call NewUndo() after changing the text.

 ******************************************************************************/

JTEUndoTabShift*
JTextEditor::GetTabShiftUndo
	(
	const JIndex	startLine,
	JBoolean*		isNew
	)
{
	JTEUndoTabShift* tabShiftUndo = NULL;

	JTEUndoBase* undo = NULL;
	if (GetCurrentUndo(&undo) &&
		(tabShiftUndo = undo->CastToJTEUndoTabShift()) != NULL &&
		tabShiftUndo->IsActive())
		{
		*isNew = kFalse;
		return tabShiftUndo;
		}
	else
		{
		tabShiftUndo = new JTEUndoTabShift(this, startLine);
		assert( tabShiftUndo != NULL );

		*isNew = kTrue;
		return tabShiftUndo;
		}
}

/******************************************************************************
 InsertText (private)

	*** Caller must call Recalc().  Nothing can be selected.

	style can be NULL.

 ******************************************************************************/

void
JTextEditor::InsertText
	(
	const JIndex			charIndex,
	const JCharacter*		text,
	const JRunArray<Font>*	style
	)
{
	const JSize textLen = strlen(text);
	if (textLen > 0)
		{
		assert( style == NULL || textLen == style->GetElementCount() );
		assert( itsSelection.IsEmpty() );

		itsBuffer->InsertSubstring(text, charIndex);
		if (style != NULL)
			{
			itsStyles->InsertElementsAtIndex(charIndex, *style, 1, style->GetElementCount());
			}
		else
			{
			itsStyles->InsertElementsAtIndex(charIndex, itsInsertionFont, textLen);
			}
		}
}

/******************************************************************************
 DeleteText (private)

	*** Caller must call Recalc().

 ******************************************************************************/

void
JTextEditor::DeleteText
	(
	const JIndexRange& range
	)
{
	itsBuffer->RemoveSubstring(range);
	itsStyles->RemoveNextElements(range.first, range.GetLength());
}

void
JTextEditor::DeleteText
	(
	const JIndex startIndex,
	const JIndex endIndex
	)
{
	itsBuffer->RemoveSubstring(startIndex, endIndex);
	itsStyles->RemoveNextElements(startIndex, endIndex - startIndex + 1);
}

/******************************************************************************
 Paginate (protected)

	Returns breakpoints for cutting text into pages.  The first breakpoint
	is always zero, and the last breakpoint is the height of the text.
	Thus, it is easy to calculate the width of what is printed on each page
	from (breakpt->GetElement(i+1) - breakpt->GetElement(i) + 1).

 ******************************************************************************/

void
JTextEditor::Paginate
	(
	const JCoordinate		pageHeight,
	JArray<JCoordinate>*	breakpts
	)
	const
{
	assert( pageHeight > 0 );

	breakpts->RemoveAll();
	breakpts->AppendElement(0);

	const JSize count = GetLineCount();

	JRunArrayIterator<LineGeometry> iter(itsLineGeom);
	LineGeometry geom;

	JIndex prev = 1, i = 0;
	JCoordinate h = 0;
	do
		{
		// find the number of strips that will fit on this page

		while (i < count && h <= pageHeight)
			{
			i++;
			const JBoolean ok = iter.Next(&geom);
			assert( ok );
			h += geom.height;
			}

		JCoordinate pageH = h;
		if (h > pageHeight && i > prev)
			{
			// The last line didn't fit on the page,
			// so leave it for the next page.

			pageH -= geom.height;
			i--;
			const JBoolean ok = iter.Prev(&geom);
			assert( ok );
			h = 0;
			}
		else if (h > pageHeight)
			{
			// The line won't fit on any page.  Put
			// as much as possible on this page and leave
			// the rest for the next page.

			pageH = pageHeight;
			h    -= pageHeight;
			}
		else
			{
			// Everything fits on the page, so there is no residual.

			h = 0;
			}

		breakpts->AppendElement(breakpts->GetLastElement() + pageH);
		prev = i;
		}
		while (i < count || h > 0);
}

/******************************************************************************
 Print

	This function could be optimized to save the necessary state so
	TESetBoundsWidth() doesn't have to be called again at the end.

 ******************************************************************************/

void
JTextEditor::Print
	(
	JPagePrinter& p
	)
{
	// avoid notifing GUI from within Recalc()
	// (We put it here to allow OpenDocument() to adjust things.)

	itsIsPrintingFlag = kTrue;

	if (!p.OpenDocument())
		{
		itsIsPrintingFlag = kFalse;
		return;
		}

	const JCoordinate headerHeight = GetPrintHeaderHeight(p);
	const JCoordinate footerHeight = GetPrintFooterHeight(p);

	// We deactivate before printing so the selection or the
	// caret won't be printed.

	const JBoolean savedActive = TEIsActive();
	TEDeactivate();

	// adjust to the width of the page

	const JCoordinate savedWidth = TEGetBoundsWidth();
	const JCoordinate pageWidth  = p.GetPageWidth();
	TESetBoundsWidth(pageWidth);

	// paginate

	JArray<JCoordinate> breakpts;
	Paginate(p.GetPageHeight()-headerHeight-footerHeight, &breakpts);

	// print each page

	const JSize pageCount = breakpts.GetElementCount() - 1;

	JBoolean cancelled = kFalse;
	for (JIndex i=1; i<=pageCount; i++)
		{
		if (!p.NewPage())
			{
			cancelled = kTrue;
			break;
			}

		if (headerHeight > 0)
			{
			DrawPrintHeader(p, headerHeight);
			p.LockHeader(headerHeight);
			}
		if (footerHeight > 0)
			{
			DrawPrintFooter(p, footerHeight);
			p.LockFooter(footerHeight);
			}

		JPoint topLeft(0, breakpts.GetElement(i));
		JPoint bottomRight(pageWidth, breakpts.GetElement(i+1));
		JRect clipRect(JPoint(0,0), bottomRight - topLeft);
		p.SetClipRect(clipRect);

		p.ShiftOrigin(-topLeft);
		TEDraw(p, JRect(topLeft,bottomRight));
		}

	if (!cancelled)
		{
		p.CloseDocument();
		}

	itsIsPrintingFlag = kFalse;

	// restore the original state and width

	if (savedActive)
		{
		TEActivate();
		}

	TESetBoundsWidth(savedWidth);
}

/******************************************************************************
 Print header and footer (virtual protected)

	Derived classes can override these functions if they want to
	print a header or a footer.

 ******************************************************************************/

JCoordinate
JTextEditor::GetPrintHeaderHeight
	(
	JPagePrinter& p
	)
	const
{
	return 0;
}

JCoordinate
JTextEditor::GetPrintFooterHeight
	(
	JPagePrinter& p
	)
	const
{
	return 0;
}

void
JTextEditor::DrawPrintHeader
	(
	JPagePrinter&		p,
	const JCoordinate	headerHeight
	)
{
}

void
JTextEditor::DrawPrintFooter
	(
	JPagePrinter&		p,
	const JCoordinate	footerHeight
	)
{
}

/******************************************************************************
 GetCmdStatus (protected)

	Returns an array indicating which commands can be performed in the
	current state.

 ******************************************************************************/

JArray<JBoolean>
JTextEditor::GetCmdStatus
	(
	JString*	crmActionText,
	JString*	crm2ActionText,
	JBoolean*	isReadOnly
	)
	const
{
	JArray<JBoolean> flags(kCmdCount);
	for (JIndex i=1; i<=kCmdCount; i++)
		{
		flags.AppendElement(kFalse);
		}

	*isReadOnly = kTrue;

	if (itsType == kStaticText || !itsActiveFlag)
		{
		return flags;
		}

	if (itsSelection.IsEmpty())
		{
		*crmActionText  = kCRMCaretActionText;
		*crm2ActionText = kCRM2CaretActionText;
		}
	else
		{
		flags.SetElement(kCopyCmd, kTrue);
		*crmActionText  = kCRMSelectionActionText;
		*crm2ActionText = kCRM2SelectionActionText;
		}
	flags.SetElement(kSelectAllCmd, kTrue);
	flags.SetElement(kToggleReadOnlyCmd, kTrue);

	if (itsType == kFullEditor)
		{
		*isReadOnly = kFalse;

		flags.SetElement(kPasteCmd, kTrue);
		if (!itsSelection.IsEmpty())
			{
			flags.SetElement(kCutCmd, kTrue);
			flags.SetElement(kDeleteSelCmd, kTrue);
			}

		if (!itsBuffer->IsEmpty())
			{
			flags.SetElement(kCleanRightMarginCmd,  kTrue);
			flags.SetElement(kCoerceRightMarginCmd, kTrue);
			flags.SetElement(kShiftSelLeftCmd,      kTrue);
			flags.SetElement(kShiftSelRightCmd,     kTrue);
			}

		if (itsUndoList != NULL)
			{
			flags.SetElement(kUndoCmd, JConvertToBoolean( itsFirstRedoIndex > 1 ));
			flags.SetElement(kRedoCmd,
				JConvertToBoolean( itsFirstRedoIndex <= itsUndoList->GetElementCount() ));
			}
		else if (itsUndo != NULL)
			{
			const JBoolean isRedo = itsUndo->IsRedo();
			flags.SetElement(kUndoCmd, JNegate(isRedo));
			flags.SetElement(kRedoCmd, isRedo);
			}
		}

	return flags;
}

/******************************************************************************
 TEDraw (protected)

	Draw everything that is visible in the given rectangle.

 ******************************************************************************/

void
JTextEditor::TEDraw
	(
	JPainter&		p,
	const JRect&	rect
	)
{
	// shade the dead-zone
/*
	if (itsLeftMarginWidth > kMinLeftMarginWidth)
		{
		const JColorIndex savedColor = p.GetPenColor();
		const JBoolean savedFill     = p.IsFilling();
		p.SetPenColor(itsColormap->GetGray90Color());
		p.SetFilling(kTrue);
		const JRect& clipRect = p.GetClipRect();
		p.Rect(0, clipRect.top, itsLeftMarginWidth-1, clipRect.height());
		p.SetPenColor(savedColor);
		p.SetFilling(savedFill);
		}
*/
	p.ShiftOrigin(itsLeftMarginWidth, 0);

	// draw the text

	TEDrawText(p, rect);

	// if DND, draw drop location and object being dragged

	if ((itsDragType == kDragAndDrop || itsDragType == kLocalDragAndDrop) &&
		itsDropLoc.charIndex > 0)
		{
		TEDrawCaret(p, itsDropLoc);

		if (itsDragType == kLocalDragAndDrop)
			{
			JRect r = CalcLocalDNDRect(itsPrevPt);
			r.Shift(-itsLeftMarginWidth, 0);
			p.SetPenColor(itsDragColor);
			p.Rect(r);
			}
		}

	// otherwise, draw insertion caret

	else if (itsActiveFlag && itsCaretVisibleFlag && itsSelection.IsEmpty() &&
			 itsType == kFullEditor)
		{
		TEDrawCaret(p, itsCaretLoc);
		}

	// clean up

	p.ShiftOrigin(-itsLeftMarginWidth, 0);
}

/******************************************************************************
 TEDrawText (private)

	Draw the text that is visible in the given rectangle.

 ******************************************************************************/

void
JTextEditor::TEDrawText
	(
	JPainter&		p,
	const JRect&	rect
	)
{
	if (IsEmpty())
		{
		return;
		}

	const JSize lineCount = GetLineCount();

	JCoordinate h;
	const JIndex startLine = CalcLineIndex(rect.top, &h);

	// draw selection region

	if (itsActiveFlag && !itsSelection.IsEmpty() && itsType != kStaticText)
		{
		TEDrawSelection(p, rect, startLine, h);
		}

	// draw text, one line at a time

	JIndex runIndex, firstInRun;
	const JBoolean found = itsStyles->FindRun(GetLineStart(startLine), &runIndex, &firstInRun);
	assert( found );

	JRunArrayIterator<LineGeometry> iter(itsLineGeom, kJIteratorStartBefore, startLine);
	LineGeometry geom;
	for (JIndex i=startLine; i<=lineCount; i++)
		{
		const JBoolean ok = iter.Next(&geom);
		assert( ok );

		TEDrawLine(p, h, geom, i, &runIndex, &firstInRun);

		h += geom.height;
		if (h >= rect.bottom)
			{
			break;
			}
		}
}

/******************************************************************************
 TEDrawLine (private)

	Draw the text on the given line.

	Updates *runIndex,*firstInRun so that they are correct for the character
	beyond the end of the line.

 ******************************************************************************/

void
JTextEditor::TEDrawLine
	(
	JPainter&			p,
	const JCoordinate	top,
	const LineGeometry	geom,
	const JIndex		lineIndex,
	JIndex*				runIndex,
	JIndex*				firstInRun
	)
{
	const JSize lineLength = GetLineLength(lineIndex);
	assert( lineLength > 0 );

	JBoolean lineEndsWithNewline = kFalse;

	JIndex startChar = GetLineStart(lineIndex);
	JIndex endChar   = startChar + lineLength - 1;
	if (itsBuffer->GetCharacter(endChar) == '\n')	// some fonts draw stuff for \n
		{
		endChar--;
		lineEndsWithNewline = kTrue;
		}

	JString s;
	JCoordinate left = 0;
	while (startChar <= endChar)
		{
		JSize runLength        = itsStyles->GetRunLength(*runIndex);
		const JSize trueRunEnd = *firstInRun + runLength-1;

		runLength -= startChar - *firstInRun;
		if (startChar + runLength-1 > endChar)
			{
			runLength = endChar - startChar + 1;
			}

		const Font& f = itsStyles->GetRunDataRef(*runIndex);
		s             = itsBuffer->GetSubstring(startChar, startChar + runLength-1);

		// If there is a tab in the string, we step up to it and take care of
		// the rest in the next iteration.

		JIndex tabIndex;
		if (s.LocateSubstring("\t", &tabIndex))
			{
			runLength = tabIndex - 1;
			if (runLength > 0)
				{
				s = itsBuffer->GetSubstring(startChar, startChar + runLength-1);
				}
			}
		else
			{
			tabIndex = 0;
			}

		if (runLength > 0)
			{
			p.SetFont(f.id, f.size, f.style);
			JCoordinate ascent,descent;
			p.GetLineHeight(&ascent, &descent);

			p.String(left, top + geom.ascent - ascent, s);

			// we only care if there is more text on the line

			if (startChar + runLength <= endChar)
				{
				left += p.GetStringWidth(s);
				}
			}
		if (tabIndex > 0)
			{
			left += GetTabWidth(startChar + runLength, left);
			runLength++;
			}

		startChar += runLength;
		if (startChar > trueRunEnd)		// move to next style run
			{
			*firstInRun = startChar;
			(*runIndex)++;
			}
		}

	// account for newline

	if (lineEndsWithNewline)
		{
		const JSize runLength = itsStyles->GetRunLength(*runIndex);
		if (startChar >= *firstInRun + runLength-1)
			{
			*firstInRun += runLength;
			(*runIndex)++;
			}
		}
}

/******************************************************************************
 TEDrawSelection (private)

	Draw the selection region.

 ******************************************************************************/

void
JTextEditor::TEDrawSelection
	(
	JPainter&			p,
	const JRect&		rect,
	const JIndex		startVisLine,
	const JCoordinate	startVisLineTop
	)
{
	assert( itsActiveFlag && !itsSelection.IsEmpty() && itsType != kStaticText );

	// calculate intersection of selection region and drawing region

	JIndex startLine = GetLineForChar(itsSelection.first);
	JIndex endLine   = GetLineForChar(itsSelection.last);

	const JIndex origStartLine = startLine;
	const JIndex origEndLine   = endLine;

	JIndex startChar;
	JCoordinate x, y;

	JCoordinate endVisLineTop;
	const JIndex endVisLine = CalcLineIndex(rect.bottom, &endVisLineTop);
	if (startVisLine > endLine || endVisLine < startLine)
		{
		return;
		}
	else if (startVisLine > startLine)
		{
		startLine = startVisLine;
		startChar = GetLineStart(startVisLine);
		x         = 0;
		y         = startVisLineTop;
		}
	else
		{
		startChar = itsSelection.first;
		x         = GetCharLeft(CaretLocation(startChar, startLine));
		y         = GetLineTop(startLine);
		}

	if (endLine > endVisLine)
		{
		endLine = endVisLine;
		}

	// draw the selection region

	const JColorIndex savedColor = p.GetPenColor();
	const JBoolean savedFill     = p.IsFilling();
	if (itsSelActiveFlag)
		{
		p.SetPenColor(itsSelectionColor);
		p.SetFilling(kTrue);
		}
	else
		{
		p.SetPenColor(itsSelectionOutlineColor);
		p.SetFilling(kFalse);
		}

	const JCoordinate xmax = JMax(itsGUIWidth, itsWidth);

	JRunArrayIterator<LineGeometry> iter(itsLineGeom, kJIteratorStartBefore, startLine);
	LineGeometry geom;
	for (JIndex i=startLine; i<=endLine; i++)
		{
		const JBoolean ok = iter.Next(&geom);
		assert( ok );

		JCoordinate w;
		JIndex endChar = GetLineEnd(i);
		if (endChar > itsSelection.last)
			{
			endChar = itsSelection.last;
			w = GetStringWidth(startChar, endChar);
			}
		else
			{
			w = xmax - x;
			}

		const JCoordinate bottom = y + geom.height - 1;
		if (itsSelActiveFlag)
			{
			// solid rectangle

			p.Rect(x, y, w, geom.height);
			}
		else if (origStartLine == origEndLine)
			{
			// empty rectangle

			p.Rect(x, y, w, geom.height);
			}
		else if (i == origStartLine)
			{
			// top of selection region

			p.Line(0,bottom, x,bottom);
			p.LineTo(x,y);
			p.LineTo(x+w,y);
			p.LineTo(x+w,bottom);
			}
		else if (i == origEndLine)
			{
			// bottom of selection region

			p.Line(0,y, 0,bottom);
			p.LineTo(w,bottom);
			p.LineTo(w,y);
			p.LineTo(xmax,y);
			}
		else
			{
			// vertical sides of selection region

			p.Line(0,y, 0,bottom);
			p.Line(xmax,y, xmax,bottom);
			}

		startChar = endChar+1;

		x  = 0;		// all lines after the first start at the far left
		y += geom.height;
		}

	// clean up

	p.SetPenColor(savedColor);
	p.SetFilling(savedFill);
}

/******************************************************************************
 TEDrawCaret (private)

 ******************************************************************************/

void
JTextEditor::TEDrawCaret
	(
	JPainter&			p,
	const CaretLocation	caretLoc
	)
{
	assert( itsType == kFullEditor );

	JCoordinate x, y1, y2;

	if (caretLoc.charIndex == itsBuffer->GetLength()+1 &&
		EndsWithNewline())									// ends with newline
		{
		x  = -1;
		y1 = GetLineBottom(caretLoc.lineIndex) + 1;
		y2 = y1 + GetEWNHeight()-1;
		}
	else
		{
		x = GetCharLeft(caretLoc) - 1;
		if (x >= itsWidth)
			{
			x = itsWidth;		// keep inside bounds
			}

		y1 = GetLineTop(caretLoc.lineIndex);
		y2 = y1 + GetLineHeight(caretLoc.lineIndex)-1;
		}

	const JColorIndex savedColor = p.GetPenColor();
	p.SetPenColor(itsCaretColor);
	p.Line(x,y1, x,y2);
	p.SetPenColor(savedColor);
}

/******************************************************************************
 TEWillDragAndDrop (protected)

 ******************************************************************************/

JBoolean
JTextEditor::TEWillDragAndDrop
	(
	const JPoint&	origPt,
	const JBoolean	extendSelection,
	const JBoolean	dropCopy
	)
	const
{
	if (!itsActiveFlag || itsType == kStaticText || !itsPerformDNDFlag ||
		itsSelection.IsEmpty() || extendSelection)
		{
		return kFalse;
		}

	const JPoint pt(origPt.x - itsLeftMarginWidth, origPt.y);
	const CaretLocation caretLoc = CalcCaretLocation(pt);
	return itsSelection.Contains(caretLoc.charIndex);
}

/******************************************************************************
 TEHandleMouseDown (protected)

 ******************************************************************************/

void
JTextEditor::TEHandleMouseDown
	(
	const JPoint&	origPt,
	const JSize		clickCount,
	const JBoolean	extendSelection,
	const JBoolean	partialWord
	)
{
	assert( itsActiveFlag );

	itsDragType         = kInvalidDrag;
	itsIsDragSourceFlag = kFalse;
	if (itsType == kStaticText)
		{
		return;
		}

	DeactivateCurrentUndo();

	const JPoint pt(origPt.x - itsLeftMarginWidth, origPt.y);
	const CaretLocation caretLoc = CalcCaretLocation(pt);

	itsStartPt = itsPrevPt = pt;

	if (!itsSelection.IsEmpty() && extendSelection &&
		caretLoc.charIndex <= itsSelection.first)
		{
		itsDragType =
			(itsPrevDragType == kInvalidDrag ? kSelectDrag : itsPrevDragType);
		itsSelectionPivot = itsSelection.last+1;
		itsPrevPt.x--;
		TEHandleMouseDrag(origPt);
		}
	else if (!itsSelection.IsEmpty() && extendSelection)
		{
		itsDragType =
			(itsPrevDragType == kInvalidDrag ? kSelectDrag : itsPrevDragType);
		itsSelectionPivot = itsSelection.first;
		itsPrevPt.x--;
		TEHandleMouseDrag(origPt);
		}
	else if (itsPerformDNDFlag && clickCount == 1 && !itsSelection.IsEmpty() &&
			 itsSelection.first <= caretLoc.charIndex &&
			 caretLoc.charIndex <= itsSelection.last+1)
		{
		itsDragType = kDragAndDrop;
		itsDropLoc  = CaretLocation(0,0);
		}
	else if (extendSelection)
		{
		itsDragType       = kSelectDrag;
		itsSelectionPivot = itsCaretLoc.charIndex;	// save this before SetSelection()

		if (caretLoc.charIndex < itsCaretLoc.charIndex)
			{
			SetSelection(caretLoc.charIndex, itsCaretLoc.charIndex-1);
			}
		else if (itsCaretLoc.charIndex < caretLoc.charIndex)
			{
			SetSelection(itsCaretLoc.charIndex, caretLoc.charIndex-1);
			}
		}
	else if (clickCount == 1)
		{
		if (caretLoc != itsCaretLoc)
			{
			SetCaretLocation(caretLoc);
			}
		itsDragType       = kSelectDrag;
		itsSelectionPivot = caretLoc.charIndex;
		}
	else if (clickCount == 2)
		{
		itsDragType = (partialWord ? kSelectPartialWordDrag : kSelectWordDrag);
		TEGetDoubleClickSelection(caretLoc, partialWord, kFalse, &itsWordSelPivot);
		SetSelection(itsWordSelPivot);
		}
	else if (clickCount == 3)
		{
		itsDragType     = kSelectLineDrag;
		itsLineSelPivot = caretLoc.lineIndex;
		SetSelection(GetLineStart(caretLoc.lineIndex), GetLineEnd(caretLoc.lineIndex));
		}

	TEUpdateDisplay();
}

/******************************************************************************
 TEHandleMouseDrag (protected)

 ******************************************************************************/

void
JTextEditor::TEHandleMouseDrag
	(
	const JPoint& origPt
	)
{
	const JPoint pt(origPt.x - itsLeftMarginWidth, origPt.y);
	if (itsDragType == kInvalidDrag || pt == itsPrevPt)
		{
		return;
		}

	TEScrollForDrag(origPt);

	const CaretLocation caretLoc = CalcCaretLocation(pt);

	if (itsDragType == kSelectDrag && caretLoc.charIndex < itsSelectionPivot)
		{
		SetSelection(caretLoc.charIndex, itsSelectionPivot-1);
		Broadcast(CaretLineChanged(caretLoc.lineIndex));
		}
	else if (itsDragType == kSelectDrag && caretLoc.charIndex == itsSelectionPivot)
		{
		SetCaretLocation(caretLoc);
		}
	else if (itsDragType == kSelectDrag && caretLoc.charIndex > itsSelectionPivot)
		{
		SetSelection(itsSelectionPivot, caretLoc.charIndex-1);
		Broadcast(CaretLineChanged(caretLoc.lineIndex));
		}
	else if (itsDragType == kSelectWordDrag || itsDragType == kSelectPartialWordDrag)
		{
		JIndexRange range;
		TEGetDoubleClickSelection(caretLoc, JI2B(itsDragType == kSelectPartialWordDrag),
								  kTrue, &range);
		SetSelection(JCovering(itsWordSelPivot, range));
		Broadcast(CaretLineChanged(caretLoc.lineIndex));
		}
	else if (itsDragType == kSelectLineDrag)
		{
		SetSelection(GetLineStart( JMin(itsLineSelPivot, caretLoc.lineIndex) ),
					 GetLineEnd(   JMax(itsLineSelPivot, caretLoc.lineIndex) ) );
		Broadcast(CaretLineChanged(caretLoc.lineIndex));
		}
	else if (itsDragType == kLocalDragAndDrop)
		{
		TERefreshCaret(itsDropLoc);
		CaretLocation dropLoc = CaretLocation(0,0);
		if (caretLoc.charIndex  <= itsSelection.first ||
			itsSelection.last+1 <= caretLoc.charIndex)
			{
			dropLoc = caretLoc;	// only drop outside selection
			}
		itsDropLoc = dropLoc;
		TERefreshCaret(itsDropLoc);
		TERefreshRect( CalcLocalDNDRect(itsPrevPt) );
		TERefreshRect( CalcLocalDNDRect(pt) );
		}
	else if (itsDragType == kDragAndDrop &&
			 (JLAbs(pt.x - itsStartPt.x) > kDebounceWidth ||
			  JLAbs(pt.y - itsStartPt.y) > kDebounceWidth))
		{
		itsDragType = kInvalidDrag;		// wait for TEHandleDNDEnter()
		if (TEBeginDND())
			{
			itsIsDragSourceFlag = kTrue;
			TECreateDragClip();
			// switches to TEHandleDND*()
			}
		else if (itsType == kFullEditor)
			{
			itsDragType = kLocalDragAndDrop;
			TECreateDragClip();
			}
		}

	itsPrevPt = pt;

	TEUpdateDisplay();
}

/******************************************************************************
 TEHandleMouseUp (protected)

 ******************************************************************************/

void
JTextEditor::TEHandleMouseUp
	(
	const JBoolean dropCopy
	)
{
	// handle local DND

	if (itsDragType == kLocalDragAndDrop && itsDropLoc.charIndex != 0)
		{
		DropSelection(itsDropLoc.charIndex, dropCopy);
		TERefreshRect( CalcLocalDNDRect(itsPrevPt) );	// get rid of rectangle
		}
	else if (itsDragType == kLocalDragAndDrop || itsDragType == kDragAndDrop)
		{
		const CaretLocation startLoc = CalcCaretLocation(itsStartPt);
		if (CalcCaretLocation(itsPrevPt) == startLoc)
			{
			SetCaretLocation(startLoc);
			}
		}

	// set itsSelectionPivot for "shift-arrow" selection

	else if (!itsSelection.IsEmpty() &&
			 (itsDragType == kSelectDrag ||
			  itsDragType == kSelectPartialWordDrag ||
			  itsDragType == kSelectWordDrag ||
			  itsDragType == kSelectLineDrag))
		{
		if ((itsDragType == kSelectWordDrag ||
			 itsDragType == kSelectPartialWordDrag) &&
			itsWordSelPivot.last  == itsSelection.last &&
			itsWordSelPivot.first != itsSelection.first)
			{
			itsSelectionPivot = itsSelection.last+1;
			}
		else if (itsDragType == kSelectLineDrag &&
				 GetLineEnd(itsLineSelPivot)   == itsSelection.last &&
				 GetLineStart(itsLineSelPivot) != itsSelection.first)
			{
			itsSelectionPivot = itsSelection.last+1;
			}

		if (itsSelectionPivot == itsSelection.last+1)
			{
			itsCaretX = GetCharLeft(CalcCaretLocation(itsSelection.first));
			}
		else
			{
			itsSelectionPivot = itsSelection.first;
			itsCaretX = GetCharRight(CalcCaretLocation(itsSelection.last));
			}
		}

	// save drag type for "extend" drag

	if (itsDragType == kSelectDrag ||
		itsDragType == kSelectPartialWordDrag ||
		itsDragType == kSelectWordDrag ||
		itsDragType == kSelectLineDrag)
		{
		itsPrevDragType = itsDragType;
		}
	else
		{
		itsPrevDragType = kInvalidDrag;
		}

	itsDragType = kInvalidDrag;
}

/******************************************************************************
 TEHandleDNDEnter (protected)

 ******************************************************************************/

void
JTextEditor::TEHandleDNDEnter()
{
	itsDragType = kDragAndDrop;
	itsDropLoc  = CaretLocation(0,0);
	TERefreshCaret(itsCaretLoc);		// hide the typing caret
}

/******************************************************************************
 TEHandleDNDHere (protected)

 ******************************************************************************/

void
JTextEditor::TEHandleDNDHere
	(
	const JPoint&	origPt,
	const JBoolean	dropOnSelf
	)
{
	const JPoint pt(origPt.x - itsLeftMarginWidth, origPt.y);
	if (itsDragType != kDragAndDrop || pt == itsPrevPt)
		{
		return;
		}

	TEScrollForDrag(origPt);
	TERefreshCaret(itsDropLoc);

	const CaretLocation caretLoc = CalcCaretLocation(pt);

	CaretLocation dropLoc = CaretLocation(0,0);
	if (!dropOnSelf ||
		caretLoc.charIndex <= itsSelection.first ||
		itsSelection.last+1 <= caretLoc.charIndex)
		{
		dropLoc = caretLoc;		// only drop outside selection
		}
	itsDropLoc = dropLoc;
	TERefreshCaret(itsDropLoc);

	itsPrevPt = pt;
	TEUpdateDisplay();
}

/******************************************************************************
 TEHandleDNDLeave (protected)

 ******************************************************************************/

void
JTextEditor::TEHandleDNDLeave()
{
	assert( itsDragType == kDragAndDrop );

	TERefreshCaret(itsDropLoc);
	itsDragType = kInvalidDrag;
}

/******************************************************************************
 TEHandleDNDDrop (protected)

	If we are not the target, the derived class has to figure out how to get
	the data from the target.  Once it has the data, it should call
	Paste(text,style).

 ******************************************************************************/

void
JTextEditor::TEHandleDNDDrop
	(
	const JPoint&	pt,
	const JBoolean	dropOnSelf,
	const JBoolean	dropCopy
	)
{
	assert( itsDragType == kDragAndDrop );

	TEHandleDNDHere(pt, dropOnSelf);
	TERefreshCaret(itsDropLoc);

	if (dropOnSelf)
		{
		itsDragType = kLocalDragAndDrop;
		TEHandleMouseUp(dropCopy);
		}
	else
		{
		SetCaretLocation(itsDropLoc);
		TEPasteDropData();

		const JIndex index = GetInsertionIndex();
		if (index > itsDropLoc.charIndex)
			{
			SetSelection(itsDropLoc.charIndex, index-1);
			}
		}

	itsDragType = kInvalidDrag;
}

/******************************************************************************
 CalcLocalDNDRect (private)

	Returns the rectangle to draw when performing local DND.
	The point should be relative to the left margin.
	The rectangle is relative to the frame.

 ******************************************************************************/

JRect
JTextEditor::CalcLocalDNDRect
	(
	const JPoint& pt
	)
	const
{
	JRect r(pt, pt);
	r.Shift(itsLeftMarginWidth, 0);
	r.Shrink(-kDraggedOutlineRadius, -kDraggedOutlineRadius);
	return r;
}

/******************************************************************************
 DropSelection (private)

	Called by TEHandleMouseUp() and JTEUndoDrop.  We modify the undo
	information even though we are private because we implement a single
	user command.

 ******************************************************************************/

void
JTextEditor::DropSelection
	(
	const JIndex	origDropLoc,
	const JBoolean	dropCopy
	)
{
	assert( !itsSelection.IsEmpty() );

	if (!dropCopy &&
		(origDropLoc == itsSelection.first ||
		 origDropLoc == itsSelection.last + 1))
		{
		return;
		}

	const JSize textLen = itsSelection.GetLength();

	JString dropText;
	JRunArray<Font> dropStyles;
	const JBoolean ok = Copy(&dropText, &dropStyles);
	assert( ok );

	JIndex dropLoc       = origDropLoc;
	JTEUndoBase* newUndo = NULL;
	if (dropCopy)
		{
		SetCaretLocation(dropLoc);
		newUndo = new JTEUndoPaste(this, textLen);
		}
	else
		{
		JIndex origIndex = itsSelection.first;
		if (dropLoc > itsSelection.first)
			{
			dropLoc -= textLen;
			}
		else
			{
			assert( dropLoc < itsSelection.first );
			origIndex += textLen;
			}

		newUndo = new JTEUndoDrop(this, origIndex, dropLoc, textLen);

		DeleteText(itsSelection);
		Recalc(itsSelection.first, 1, kTrue);
		}
	assert( newUndo != NULL );

	itsSelection.SetToNothing();
	InsertText(dropLoc, dropText, &dropStyles);
	Recalc(dropLoc, textLen, kFalse);
	SetSelection(dropLoc, dropLoc + textLen-1);
	TEScrollToSelection(kFalse);

	NewUndo(newUndo);
}

/******************************************************************************
 GetDragClip (protected)

	Returns kTrue if our internal drag clipboard contains something.

	style can be NULL.

 ******************************************************************************/

JBoolean
JTextEditor::GetDragClip
	(
	const JString**			text,
	const JRunArray<Font>**	style
	)
	const
{
	if (itsDragText != NULL && itsDragStyle != NULL)
		{
		if (itsDragText->IsEmpty())
			{
			const JBoolean ok = GetSelection(itsDragText, itsDragStyle);
			assert( ok );
			}

		*text = itsDragText;
		if (style != NULL)
			{
			*style = itsDragStyle;
			}
		return kTrue;
		}
	else
		{
		*text = NULL;
		if (style != NULL)
			{
			*style = NULL;
			}
		return kFalse;
		}
}

/******************************************************************************
 TECreateDragClip (private)

	Allocate itsDragText and itsDragStyle.

 ******************************************************************************/

void
JTextEditor::TECreateDragClip()
{
	assert( (itsDragText == NULL && itsDragStyle == NULL) ||
			(itsDragText != NULL && itsDragStyle != NULL) );

	if (itsDragText == NULL)
		{
		itsDragText = new JString;
		assert( itsDragText != NULL );

		itsDragStyle = new JRunArray<Font>;
		assert( itsDragStyle != NULL );
		}
	else
		{
		itsDragText->Clear();
		itsDragStyle->RemoveAll();
		}
}

/******************************************************************************
 TEClearDragClip (protected)

	Delete itsDragText and itsDragStyle.

 ******************************************************************************/

void
JTextEditor::TEClearDragClip()
{
	delete itsDragText;
	itsDragText = NULL;

	delete itsDragStyle;
	itsDragStyle = NULL;
}

/******************************************************************************
 TEGetDoubleClickSelection (private)

	Select the word that was clicked on.  By computing the end of the word
	found by GetWordStart(), we avoid selecting two words when the user
	double clicks on the space between them.

 ******************************************************************************/

void
JTextEditor::TEGetDoubleClickSelection
	(
	const CaretLocation	caretLoc,
	const JBoolean		partialWord,
	const JBoolean		dragging,
	JIndexRange*		range
	)
{
	JIndex startIndex, endIndex;
	if (partialWord)
		{
		startIndex = GetPartialWordStart(caretLoc.charIndex);
		endIndex   = GetPartialWordEnd(startIndex);
		}
	else
		{
		startIndex = GetWordStart(caretLoc.charIndex);
		endIndex   = GetWordEnd(startIndex);
		}

	// To select the word, the caret location should be inside the word
	// or on the character following the word.

	const JSize bufLength = itsBuffer->GetLength();

	if ((startIndex <= caretLoc.charIndex && caretLoc.charIndex <= endIndex) ||
		(!dragging &&
		 ((caretLoc.charIndex == endIndex+1  && IndexValid(endIndex+1)) ||
		  (caretLoc.charIndex == bufLength+1 && endIndex == bufLength))) )
		{
		range->Set(startIndex, endIndex);
		}

	// Otherwise, we select the character that was clicked on

	else
		{
		range->first = range->last = JMin(caretLoc.charIndex, bufLength);
		if (range->first > 1 &&
			itsBuffer->GetCharacter(range->first)   == '\n' &&
			itsBuffer->GetCharacter(range->first-1) != '\n')
			{
			(range->first)--;
			(range->last)--;
			}
		}
}

/******************************************************************************
 TEHitSamePart

 ******************************************************************************/

JBoolean
JTextEditor::TEHitSamePart
	(
	const JPoint& pt1,
	const JPoint& pt2
	)
	const
{
	const CaretLocation loc1 = CalcCaretLocation(pt1);
	const CaretLocation loc2 = CalcCaretLocation(pt2);
	return JConvertToBoolean(loc1 == loc2);
}

/******************************************************************************
 TEHandleKeyPress (protected)

	Returns kTrue if the key was processed.

 ******************************************************************************/

JBoolean
JTextEditor::TEHandleKeyPress
	(
	const JCharacter	origKey,
	const JBoolean		selectText,
	const CaretMotion	motion
	)
{
	assert( itsActiveFlag );
	assert( (!itsSelection.IsEmpty() && itsCaretLoc.charIndex == 0) ||
			( itsSelection.IsEmpty() && itsCaretLoc.charIndex >  0) );

	if (itsType == kStaticText)
		{
		return kFalse;
		}
	else if (origKey == kJEscapeKey && itsDragType == kLocalDragAndDrop)
		{
		itsDragType = kInvalidDrag;
		TERefresh();
		return kTrue;
		}
	else if (TEIsDragging())
		{
		return kTrue;
		}

	// pre-processing

	JCharacter key = origKey;
	if (key == '\r')
		{
		key = '\n';
		}

	const JSize bufLength = itsBuffer->GetLength();

	// We select text by selecting to where the caret ends up.

	const JBoolean isArrowKey = JI2B(
		key == kJLeftArrow || key == kJRightArrow ||
		key == kJUpArrow   || key == kJDownArrow);
	const JBoolean willSelectText = JI2B( selectText && isArrowKey );

	if (willSelectText)
		{
		JBoolean restoreCaretX        = kTrue;
		const JCoordinate savedCaretX = itsCaretX;

		if (!itsSelection.IsEmpty() && itsSelectionPivot == itsSelection.last+1)
			{
			SetCaretLocation(itsSelection.first);
			}
		else if (!itsSelection.IsEmpty() && itsSelectionPivot == itsSelection.first)
			{
			SetCaretLocation(itsSelection.last+1);
			}
		else if (!itsSelection.IsEmpty())	// SetSelection() was called by outsider
			{
			itsSelectionPivot = itsSelection.first;
			restoreCaretX     = kFalse;
			SetCaretLocation(itsSelection.last+1);
			}
		else
			{
			itsSelectionPivot = itsCaretLoc.charIndex;
			}

		if (restoreCaretX && (key == kJUpArrow || key == kJDownArrow))
			{
			itsCaretX = savedCaretX;
			}
		}
	else if (itsType == kSelectableText && !isArrowKey)
		{
		return kFalse;
		}

	// left arrow

	if (key == kJLeftArrow && motion == kMoveByLine)
		{
		GoToBeginningOfLine();
		}

	else if (key == kJLeftArrow && motion == kMoveByWord && !itsSelection.IsEmpty())
		{
		SetCaretLocation(GetWordStart(itsSelection.first));				// works for zero
		}
	else if (key == kJLeftArrow && motion == kMoveByWord)
		{
		SetCaretLocation(GetWordStart(itsCaretLoc.charIndex-1));		// works for zero
		}

	else if (key == kJLeftArrow && motion == kMoveByPartialWord && !itsSelection.IsEmpty())
		{
		SetCaretLocation(GetPartialWordStart(itsSelection.first));		// works for zero
		}
	else if (key == kJLeftArrow && motion == kMoveByPartialWord)
		{
		SetCaretLocation(GetPartialWordStart(itsCaretLoc.charIndex-1));	// works for zero
		}

	else if (key == kJLeftArrow && !itsSelection.IsEmpty())
		{
		SetCaretLocation(itsSelection.first);
		}
	else if (key == kJLeftArrow)
		{
		if (itsCaretLoc.charIndex > 1)
			{
			SetCaretLocation(itsCaretLoc.charIndex-1);
			}
		else
			{
			SetCaretLocation(1);	// scroll to it
			}
		}

	// right arrow

	else if (key == kJRightArrow && motion == kMoveByLine)
		{
		GoToEndOfLine();
		}

	else if (key == kJRightArrow && motion == kMoveByWord && !itsSelection.IsEmpty())
		{
		SetCaretLocation(GetWordEnd(itsSelection.last)+1);
		}
	else if (key == kJRightArrow && motion == kMoveByWord)
		{
		SetCaretLocation(GetWordEnd(itsCaretLoc.charIndex)+1);
		}

	else if (key == kJRightArrow && motion == kMoveByPartialWord && !itsSelection.IsEmpty())
		{
		SetCaretLocation(GetPartialWordEnd(itsSelection.last)+1);
		}
	else if (key == kJRightArrow && motion == kMoveByPartialWord)
		{
		SetCaretLocation(GetPartialWordEnd(itsCaretLoc.charIndex)+1);
		}

	else if (key == kJRightArrow && !itsSelection.IsEmpty())
		{
		SetCaretLocation(itsSelection.last+1);
		}
	else if (key == kJRightArrow)
		{
		if (itsCaretLoc.charIndex <= bufLength)
			{
			SetCaretLocation(itsCaretLoc.charIndex+1);
			}
		else
			{
			SetCaretLocation(bufLength+1);	// scroll to it
			}
		}

	// up arrow

	else if (key == kJUpArrow && motion == kMoveByLine)
		{
		SetCaretLocation(1);
		}

	else if (key == kJUpArrow && motion == kMoveByWord && !itsSelection.IsEmpty())
		{
		SetCaretLocation(GetParagraphStart(itsSelection.first-1));
		}
	else if (key == kJUpArrow && motion == kMoveByWord)
		{
		SetCaretLocation(GetParagraphStart(itsCaretLoc.charIndex-1));
		}

	else if (key == kJUpArrow && !itsSelection.IsEmpty())
		{
		SetCaretLocation(itsSelection.first);
		}
	else if (key == kJUpArrow && itsCaretLoc.charIndex == bufLength+1 &&
			 EndsWithNewline())									// ends with newline
		{
		SetCaretLocation(GetLineStart(itsCaretLoc.lineIndex));
		}
	else if (key == kJUpArrow && itsCaretLoc.lineIndex > 1)
		{
		MoveCaretVert(-1);
		}
	else if (key == kJUpArrow)
		{
		SetCaretLocation(1);
		}

	// down arrow

	else if (key == kJDownArrow && motion == kMoveByLine)
		{
		SetCaretLocation(bufLength+1);
		}

	else if (key == kJDownArrow && motion == kMoveByWord && !itsSelection.IsEmpty())
		{
		SetCaretLocation(GetParagraphEnd(itsSelection.last+1)+1);
		}
	else if (key == kJDownArrow && motion == kMoveByWord)
		{
		SetCaretLocation(GetParagraphEnd(itsCaretLoc.charIndex)+1);
		}

	else if (key == kJDownArrow && !itsSelection.IsEmpty())
		{
		SetCaretLocation(itsSelection.last+1);
		}
	else if (key == kJDownArrow && itsCaretLoc.lineIndex < GetLineCount())
		{
		MoveCaretVert(+1);
		}
	else if (key == kJDownArrow)
		{
		SetCaretLocation(bufLength+1);
		}

	// delete

	else if (key == kJDeleteKey && !itsSelection.IsEmpty())
		{
		DeleteSelection();
		}
	else if (key == kJDeleteKey && itsCaretLoc.charIndex > 1)
		{
		JBoolean isNew;
		JTEUndoTyping* typingUndo = GetTypingUndo(&isNew);
		typingUndo->HandleDelete();

		const Font f = itsStyles->GetElement(itsCaretLoc.charIndex-1);	// preserve font
		DeleteText(itsCaretLoc.charIndex-1, itsCaretLoc.charIndex-1);
		Recalc(itsCaretLoc.charIndex-1, 1, kTrue);
		SetCaretLocation(itsCaretLoc.charIndex-1);
		if (itsPasteStyledTextFlag)
			{
			itsInsertionFont = f;
			}

		typingUndo->Activate();		// cancel SetCaretLocation()
		if (isNew)
			{
			NewUndo(typingUndo);
			}
		}

	// forward delete

	else if (key == kJForwardDeleteKey && !itsSelection.IsEmpty())
		{
		DeleteSelection();
		}
	else if (key == kJForwardDeleteKey && itsCaretLoc.charIndex <= bufLength)
		{
		JBoolean isNew;
		JTEUndoTyping* typingUndo = GetTypingUndo(&isNew);
		typingUndo->HandleFwdDelete();

		DeleteText(itsCaretLoc.charIndex, itsCaretLoc.charIndex);
		Recalc(itsCaretLoc, 1, kTrue);
		SetCaretLocation(itsCaretLoc.charIndex);

		typingUndo->Activate();		// cancel SetCaretLocation()
		if (isNew)
			{
			NewUndo(typingUndo);
			}
		}

	// insert character

	else if (isprint(key) || key == '\n' || key == '\t' || (key & 0x80))
		{
		JBoolean isNew;
		JTEUndoTyping* typingUndo = GetTypingUndo(&isNew);
		typingUndo->HandleCharacter();

		if (!itsSelection.IsEmpty())
			{
			itsInsertionFont = itsStyles->GetElement(itsSelection.first);
			DeleteText(itsSelection);
			itsCaretLoc = CalcCaretLocation(itsSelection.first);
			itsSelection.SetToNothing();
			}
		JCharacter s[2] = {key, '\0'};
		InsertText(itsCaretLoc.charIndex, s);
		Recalc(itsCaretLoc, 1, kFalse);
		SetCaretLocation(itsCaretLoc.charIndex+1);

		typingUndo->Activate();		// cancel SetCaretLocation()

		if (key == '\n' && itsAutoIndentFlag)
			{
			AutoIndent(typingUndo);
			}

		if (isNew)
			{
			NewUndo(typingUndo);
			}
		}

	// finish selection process

	if (willSelectText)
		{
		if (itsCaretLoc.charIndex < itsSelectionPivot)
			{
			SetSelection(itsCaretLoc.charIndex, itsSelectionPivot-1);
			}
		else if (itsCaretLoc.charIndex > itsSelectionPivot)
			{
			SetSelection(itsSelectionPivot, itsCaretLoc.charIndex-1);
			}

		itsPrevDragType = kSelectDrag;
		}

	// We redraw the display immediately because it is very disconcerting
	// when the display does not instantly show the changes.

	TEUpdateDisplay();
	return kTrue;
}

/******************************************************************************
 AutoIndent (private)

	Insert the "rest" prefix based on the previous line.  If the previous
	line is nothing but whitespace, clear it.

 ******************************************************************************/

void
JTextEditor::AutoIndent
	(
	JTEUndoTyping* typingUndo
	)
{
	assert( itsSelection.IsEmpty() );

	// Move up one line if we are not at the very end of the text.

	JIndex lineIndex = itsCaretLoc.lineIndex;
	if (itsCaretLoc.charIndex <= itsBuffer->GetLength())	// ends with newline
		{
		lineIndex--;
		}

	// Skip past lines that were created by wrapping the text.
	// (This is faster than using GetParagraphStart() because then we would
	//  have to call GetLineForChar(), which is a search.  GetLineStart()
	//  is an array lookup.)

	JIndex firstChar = GetLineStart(lineIndex);
	while (lineIndex > 1 && itsBuffer->GetCharacter(firstChar-1) != '\n')
		{
		lineIndex--;
		firstChar = GetLineStart(lineIndex);
		}

	// Calculate the prefix range for the original line.

	const JIndex lastChar = GetParagraphEnd(firstChar) - 1;
	assert( itsBuffer->GetCharacter(lastChar+1) == '\n' );

	JIndex firstTextChar = firstChar;
	JString linePrefix;
	JSize prefixLength;
	JIndex ruleIndex = 0;
	if (lastChar < firstChar ||
		!CRMGetPrefix(&firstTextChar, lastChar,
					  &linePrefix, &prefixLength, &ruleIndex))
		{
		firstTextChar = lastChar+1;
		if (firstTextChar > firstChar)
			{
			linePrefix = itsBuffer->GetSubstring(firstChar, firstTextChar-1);
			}
		}
	else if (CRMLineMatchesRest(JIndexRange(firstChar, lastChar)))
		{
		// CRMBuildRestPrefix() will complain if we pass in the entire
		// line, so we give it only the whitespace.

		CRMRuleList* savedRuleList = itsCRMRuleList;
		itsCRMRuleList             = NULL;
		firstTextChar              = firstChar;
		ruleIndex                  = 0;
		CRMGetPrefix(&firstTextChar, lastChar,
					 &linePrefix, &prefixLength, &ruleIndex);
		itsCRMRuleList = savedRuleList;
		}

	// Generate the prefix and include it in the undo object.

	if (firstTextChar > firstChar)
		{
		linePrefix   = CRMBuildRestPrefix(linePrefix, ruleIndex, &prefixLength);
		prefixLength = linePrefix.GetLength();

		typingUndo->HandleCharacters(prefixLength);

		InsertText(itsCaretLoc.charIndex, linePrefix);
		Recalc(itsCaretLoc, prefixLength, kFalse);

		JIndex newCaretChar = itsCaretLoc.charIndex + prefixLength;

		// check the initial whitespace range

		JIndex lastWSChar = firstChar;
		while (1)
			{
			const JCharacter c = itsBuffer->GetCharacter(lastWSChar);
			if ((c != ' ' && c != '\t') || lastWSChar > lastChar)		// can't use isspace() because '\n' stops us
				{
				lastWSChar--;
				break;
				}
			lastWSChar++;
			}

		// if the line is blank, clear it

		if (lastWSChar >= firstChar && itsBuffer->GetCharacter(lastWSChar+1) == '\n')
			{
			typingUndo->HandleAutoIndentDelete(firstChar, lastWSChar);

			DeleteText(firstChar, lastWSChar);
			Recalc(CaretLocation(firstChar, lineIndex), 1, kTrue);

			newCaretChar -= lastWSChar - firstChar + 1;
			}

		SetCaretLocation(newCaretChar);

		// re-activate undo after SetCaretLocation()

		typingUndo->Activate();
		}
}

/******************************************************************************
 ContainsIllegalChars (static)

	Returns kTrue if the given text contains characters that we will not
	accept:  00-08, 0B, 0E-1F, 7F

	We accept form feed (0C) because PrintPlainText() converts it to space.

	We accept all characters above 0x7F because they provide useful
	(though hopelessly system dependent) extensions to the character set.

 ******************************************************************************/

static const JRegex illegalCharRegex("[\x00-\x08\x0B\x0E-\x1F\x7F]+", 11);

JBoolean
JTextEditor::ContainsIllegalChars
	(
	const JCharacter*	text,
	const JSize			length
	)
{
	return illegalCharRegex.MatchWithin(text, JIndexRange(1,length));
}

/******************************************************************************
 RemoveIllegalChars (static)

	Returns kTrue if we had to remove any characters that
	ContainsIllegalChars() would flag.

	If the user cancels, we toss the entire string and return kTrue.

	style can be NULL or empty.

 ******************************************************************************/

JBoolean
JTextEditor::RemoveIllegalChars
	(
	JString*			text,
	JRunArray<Font>*	style
	)
{
	assert( style == NULL || style->IsEmpty() ||
			style->GetElementCount() == text->GetLength() );

	const JSize origTextLength = text->GetLength();
	const JSize saveBlockSize  = text->GetBlockSize();
	text->SetBlockSize(origTextLength);			// avoid realloc

	JLatentPG pg(100);
	pg.FixedLengthProcessBeginning(origTextLength,
				"Removing illegal characters...", kTrue, kFalse);

	JIndexRange remainder(1, text->GetLength());
	JIndexRange illegal;
	while (illegalCharRegex.MatchWithin(*text, remainder, &illegal))
		{
		text->RemoveSubstring(illegal);
		if (style != NULL && !style->IsEmpty())
			{
			style->RemoveNextElements(illegal.first, illegal.GetLength());
			}

		const JSize pgDelta = (illegal.first - remainder.first) + illegal.GetLength();
		remainder.Set(illegal.first, text->GetLength());

		if (!pg.IncrementProgress(pgDelta))
			{
			text->RemoveSubstring(remainder);
			if (style != NULL)
				{
				style->RemoveNextElements(remainder.first, remainder.GetLength());
				}
			break;
			}
		}

	pg.ProcessFinished();

	text->SetBlockSize(saveBlockSize);
	return JConvertToBoolean( text->GetLength() < origTextLength );
}

/******************************************************************************
 GetWordStart

	Return the index of the first character of the word at the given location.
	This function is required to work for charIndex == 0.

 ******************************************************************************/

JIndex
JTextEditor::GetWordStart
	(
	const JIndex charIndex
	)
	const
{
	if (itsBuffer->IsEmpty() || charIndex <= 1)
		{
		return 1;
		}

	JIndex i = JMin(charIndex, itsBuffer->GetLength());
	if (!IsCharacterInWord(*itsBuffer, i))
		{
		i--;
		while (i >= 1 && !IsCharacterInWord(*itsBuffer, i))
			{
			i--;
			}
		}

	while (i >= 1 && IsCharacterInWord(*itsBuffer, i))
		{
		i--;
		}
	return i+1;
}

/******************************************************************************
 GetWordEnd

	Return the index of the last character of the word at the given location.
	This function is required to work for charIndex > buffer length.

 ******************************************************************************/

JIndex
JTextEditor::GetWordEnd
	(
	const JIndex charIndex
	)
	const
{
	if (itsBuffer->IsEmpty())
		{
		return 1;
		}

	const JSize bufLen = itsBuffer->GetLength();
	if (charIndex >= bufLen)
		{
		return bufLen;
		}

	JIndex i = charIndex;
	if (!IsCharacterInWord(*itsBuffer, i))
		{
		i++;
		while (i <= bufLen && !IsCharacterInWord(*itsBuffer, i))
			{
			i++;
			}
		}

	while (i <= bufLen && IsCharacterInWord(*itsBuffer, i))
		{
		i++;
		}
	return i-1;
}

/******************************************************************************
 IsCharacterInWord (virtual protected)

	Returns kTrue if the given character should be considered part of
	a word.  The default definition is [A-Za-z0-9].

 ******************************************************************************/

JBoolean
JTextEditor::IsCharacterInWord
	(
	const JString&	text,
	const JIndex	charIndex
	)
	const
{
	return JConvertToBoolean( isalnum(text.GetCharacter(charIndex)) );
}

/******************************************************************************
 GetPartialWordStart

	Return the index of the first character of the partial word at the given
	location.  This function is required to work for charIndex == 0.

	Example:  get_word Get142TheWordABCGood ABCDe
	          ^   ^    ^  ^  ^  ^   ^  ^    ^  ^

 ******************************************************************************/

JIndex
JTextEditor::GetPartialWordStart
	(
	const JIndex charIndex
	)
	const
{
	if (itsBuffer->IsEmpty() || charIndex <= 1)
		{
		return 1;
		}

	JIndex i = JMin(charIndex, itsBuffer->GetLength());
	if (!isalnum(itsBuffer->GetCharacter(i)))
		{
		i--;
		while (i >= 1 && !isalnum(itsBuffer->GetCharacter(i)))
			{
			i--;
			}
		}

	if (i < 2)
		{
		return 1;
		}

	JCharacter prev     = itsBuffer->GetCharacter(i);
	JBoolean foundLower = JI2B(islower(prev));
	while (i > 1)
		{
		const JCharacter c = itsBuffer->GetCharacter(i-1);
		foundLower         = JI2B(foundLower || islower(c));
		if (!isalnum(c) ||
			(isupper(prev) && islower(c)) ||
			(isupper(prev) && isupper(c) && foundLower) ||
			(isalpha(prev) && isdigit(c)) ||
			(isdigit(prev) && isalpha(c)))
			{
			break;
			}

		prev = c;
		i--;
		}

	return i;
}

/******************************************************************************
 GetPartialWordEnd

	Return the index of the last character of the partial word at the given
	location.  This function is required to work for charIndex > buffer length.

	Example:  get_word Get142TheWordABCGood
	            ^    ^   ^  ^  ^   ^  ^   ^

 ******************************************************************************/

JIndex
JTextEditor::GetPartialWordEnd
	(
	const JIndex charIndex
	)
	const
{
	if (itsBuffer->IsEmpty())
		{
		return 1;
		}

	const JSize bufLen = itsBuffer->GetLength();
	if (charIndex >= bufLen)
		{
		return bufLen;
		}

	JIndex i = charIndex;
	if (!isalnum(itsBuffer->GetCharacter(i)))
		{
		i++;
		while (i <= bufLen && !isalnum(itsBuffer->GetCharacter(i)))
			{
			i++;
			}
		}

	if (i >= bufLen)
		{
		return bufLen;
		}

	JCharacter prev = itsBuffer->GetCharacter(i);
	while (i < bufLen)
		{
		const JCharacter c = itsBuffer->GetCharacter(i+1);
		if (!isalnum(c) ||
			(islower(prev) && isupper(c)) ||
			(isalpha(prev) && isdigit(c)) ||
			(isdigit(prev) && isalpha(c)) ||
			(i < bufLen-1 && isupper(prev) && isupper(c) &&
			 islower(itsBuffer->GetCharacter(i+2))))
			{
			break;
			}

		prev = c;
		i++;
		}

	return i;
}

/******************************************************************************
 GetParagraphStart

	Return the index of the first character in the paragraph that contains
	the character at the given location.  This function is required to work
	for charIndex == 0.

 ******************************************************************************/

JIndex
JTextEditor::GetParagraphStart
	(
	const JIndex charIndex
	)
	const
{
	if (itsBuffer->IsEmpty() || charIndex <= 1)
		{
		return 1;
		}

	JIndex i = JMin(charIndex, itsBuffer->GetLength());
	while (i > 1 && itsBuffer->GetCharacter(i-1) != '\n')
		{
		i--;
		}

	return i;
}

/******************************************************************************
 GetParagraphEnd

	Return the index of the newline that ends the paragraph that contains
	the character at the given location.  This function is required to work
	for charIndex > buffer length.

 ******************************************************************************/

JIndex
JTextEditor::GetParagraphEnd
	(
	const JIndex charIndex
	)
	const
{
	if (itsBuffer->IsEmpty())
		{
		return 1;
		}

	const JSize bufLen = itsBuffer->GetLength();
	if (charIndex >= bufLen)
		{
		return bufLen;
		}

	JIndex i = charIndex;
	while (i < bufLen && itsBuffer->GetCharacter(i) != '\n')
		{
		i++;
		}

	return i;
}

/******************************************************************************
 SetCaretLocation

	Move the caret to the specified point.

 ******************************************************************************/

void
JTextEditor::SetCaretLocation
	(
	const JIndex origCharIndex
	)
{
	JIndex charIndex = JMax(origCharIndex, (JIndex) 1);
	charIndex        = JMin(charIndex, itsBuffer->GetLength()+1);

	SetCaretLocation( CalcCaretLocation(charIndex) );
}

// private

void
JTextEditor::SetCaretLocation
	(
	const CaretLocation caretLoc
	)
{
	DeactivateCurrentUndo();

	const JBoolean hadSelection      = JNegate( itsSelection.IsEmpty() );
	const CaretLocation origCaretLoc = itsCaretLoc;

	if (hadSelection)
		{
		TERefreshLines(GetLineForChar(itsSelection.first),
					   GetLineForChar(itsSelection.last));
		}

	itsSelection.SetToNothing();
	itsCaretLoc = caretLoc;
	itsCaretX   = GetCharLeft(itsCaretLoc);

	if (hadSelection || origCaretLoc != itsCaretLoc || !itsPasteStyledTextFlag)
		{
		itsInsertionFont = CalcInsertionFont(itsCaretLoc.charIndex);
		}

	if (!TEScrollTo(itsCaretLoc))
		{
		TERefreshCaret(origCaretLoc);
		TERefreshCaret(itsCaretLoc);
		}

	if (hadSelection || origCaretLoc.lineIndex != itsCaretLoc.lineIndex)
		{
		Broadcast(CaretLineChanged(itsCaretLoc.lineIndex));
		}

	TECaretShouldBlink(kTrue);
}

/******************************************************************************
 GoToLine

 ******************************************************************************/

void
JTextEditor::GoToLine
	(
	const JIndex lineIndex
	)
{
	JIndex trueIndex      = lineIndex;
	const JSize lineCount = GetLineCount();

	CaretLocation caretLoc;
	if (trueIndex > lineCount && EndsWithNewline())		// ends with newline
		{
		caretLoc.charIndex = itsBuffer->GetLength() + 1;
		caretLoc.lineIndex = lineCount;
		}
	else
		{
		if (trueIndex == 0)
			{
			trueIndex = 1;
			}
		else if (trueIndex > lineCount)
			{
			trueIndex = lineCount;
			}
		caretLoc.charIndex = GetLineStart(trueIndex);
		caretLoc.lineIndex = trueIndex;
		}

	TEScrollToRect(CalcCaretRect(caretLoc), kTrue);
	SetCaretLocation(caretLoc);
}

/******************************************************************************
 SelectLine

 ******************************************************************************/

void
JTextEditor::SelectLine
	(
	const JIndex lineIndex
	)
{
	GoToLine(lineIndex);
	assert( itsSelection.IsEmpty() );
	SetSelection(GetLineStart(itsCaretLoc.lineIndex),
				 GetLineEnd(itsCaretLoc.lineIndex));
}

/******************************************************************************
 GoToBeginningOfLine

 ******************************************************************************/

void
JTextEditor::GoToBeginningOfLine()
{
	CaretLocation caretLoc;
	if (!itsSelection.IsEmpty())
		{
		caretLoc = CalcCaretLocation(itsSelection.first);
		}
	else
		{
		caretLoc = itsCaretLoc;
		}

	if (caretLoc.charIndex == itsBuffer->GetLength()+1 &&
		EndsWithNewline())										// ends with newline
		{
		// set current value so we scroll to it
		}
	else if (itsMoveToFrontOfTextFlag && !itsBuffer->IsEmpty())
		{
		const JIndex firstChar = GetLineStart(caretLoc.lineIndex);
		JIndex lastChar        = GetLineEnd(caretLoc.lineIndex);
		if (itsBuffer->GetCharacter(lastChar) == '\n')
			{
			lastChar--;
			}

		JIndex firstTextChar = firstChar;
		JString linePrefix;
		JSize prefixLength;
		JIndex ruleIndex = 0;
		if (lastChar < firstChar ||
			!CRMGetPrefix(&firstTextChar, lastChar,
						  &linePrefix, &prefixLength, &ruleIndex) ||
			CRMLineMatchesRest(JIndexRange(firstChar, lastChar)))
			{
			firstTextChar = lastChar+1;
			}

		caretLoc.charIndex =
			caretLoc.charIndex <= firstTextChar ? firstChar : firstTextChar;
		}
	else
		{
		caretLoc.charIndex = GetLineStart(caretLoc.lineIndex);
		}

	SetCaretLocation(caretLoc);
}

/******************************************************************************
 GoToEndOfLine

 ******************************************************************************/

void
JTextEditor::GoToEndOfLine()
{
	if (!itsSelection.IsEmpty())
		{
		const JIndex lineIndex = GetLineForChar(itsSelection.last);
		const JIndex charIndex = GetLineEnd(lineIndex);
		if (isspace(itsBuffer->GetCharacter(charIndex)))
			{
			SetCaretLocation(charIndex);
			}
		else
			{
			SetCaretLocation(charIndex+1);
			}
		}
	else if (itsCaretLoc.charIndex == itsBuffer->GetLength()+1 &&
			 EndsWithNewline())										// ends with newline
		{
		SetCaretLocation(itsCaretLoc);	// scroll to it
		}
	else if (!itsBuffer->IsEmpty())
		{
		const JIndex charIndex = GetLineEnd(itsCaretLoc.lineIndex);
		const JCharacter c     = itsBuffer->GetCharacter(charIndex);
		if (isspace(c) && (charIndex < itsBuffer->GetLength() || c == '\n'))
			{
			SetCaretLocation(charIndex);
			}
		else
			{
			SetCaretLocation(charIndex+1);
			}
		}
}

/******************************************************************************
 TEScrollTo (private)

	Scroll to the specified insertion point.  Returns kTrue if scrolling
	was actually necessary.

 ******************************************************************************/

JBoolean
JTextEditor::TEScrollTo
	(
	const CaretLocation caretLoc
	)
{
	return TEScrollToRect(CalcCaretRect(caretLoc), kFalse);
}

/******************************************************************************
 CalcCaretRect (private)

	Calculate the rectangle enclosing the caret when it is at the given
	location.  We set the width of the rectangle to be the left margin
	width so that the entire left margin will scroll into view when the
	caret is at the far left.

 ******************************************************************************/

JRect
JTextEditor::CalcCaretRect
	(
	const CaretLocation caretLoc
	)
	const
{
	JRect r;

	if (caretLoc.charIndex == itsBuffer->GetLength()+1 &&
		EndsWithNewline())								// ends with newline
		{
		r.top    = GetLineBottom(caretLoc.lineIndex) + 1;
		r.bottom = r.top + GetEWNHeight();
		r.left   = 0;
		r.right  = itsLeftMarginWidth + kRightMarginWidth;
		}
	else
		{
		r.top    = GetLineTop(caretLoc.lineIndex);
		r.bottom = r.top + GetLineHeight(caretLoc.lineIndex);
		r.left   = GetCharLeft(caretLoc);
		r.right  = r.left + itsLeftMarginWidth + kRightMarginWidth;
		}

	return r;
}

/******************************************************************************
 TEScrollToSelection (private)

	Scroll to make the selection or caret visible.  Returns kTrue if scrolling
	was actually necessary.

 ******************************************************************************/

JBoolean
JTextEditor::TEScrollToSelection
	(
	const JBoolean centerInDisplay
	)
{
	if (!itsSelection.IsEmpty())
		{
		const CaretLocation start = CalcCaretLocation(itsSelection.first);
		const CaretLocation end   = CalcCaretLocation(itsSelection.last);

		JRect r;
		r.top    = GetLineTop(start.lineIndex);
		r.bottom = GetLineBottom(end.lineIndex);
		if (start.lineIndex == end.lineIndex)
			{
			r.left  = GetCharLeft(start) + itsLeftMarginWidth;
			r.right = GetCharRight(end) + itsLeftMarginWidth;
			}
		else
			{
			r.left  = 0;
			r.right = itsLeftMarginWidth + itsWidth + kRightMarginWidth;
			}

		if (itsSelection.last == itsBuffer->GetLength()+1 &&
			EndsWithNewline())								// ends with newline
			{
			const JCoordinate dh = GetEWNHeight();
			r.top    += dh;
			r.bottom += dh;
			r.left    = 0;
			r.right   = itsLeftMarginWidth + itsWidth + kRightMarginWidth;
			}

		return TEScrollToRect(r, centerInDisplay);
		}
	else
		{
		return TEScrollToRect(CalcCaretRect(itsCaretLoc), centerInDisplay);
		}
}

/******************************************************************************
 TERefreshLines (protected)

	Redraw the specified range of lines.

 ******************************************************************************/

void
JTextEditor::TERefreshLines
	(
	const JIndex firstLine,
	const JIndex lastLine
	)
{
	const JRect r(GetLineTop(firstLine), 0, GetLineBottom(lastLine)+1,
				  itsLeftMarginWidth + JMax(itsGUIWidth, itsWidth) +
				  kRightMarginWidth);
	TERefreshRect(r);
}

/******************************************************************************
 TERefreshCaret (private)

 ******************************************************************************/

void
JTextEditor::TERefreshCaret
	(
	const CaretLocation caretLoc
	)
{
	if (caretLoc.charIndex == itsBuffer->GetLength()+1 &&
		EndsWithNewline())								// ends with newline
		{
		const JRect r(itsHeight - GetEWNHeight(), 0, itsHeight,
					  itsLeftMarginWidth + JMax(itsGUIWidth, itsWidth) +
					  kRightMarginWidth);
		TERefreshRect(r);
		}
	else if (itsLineStarts->IndexValid(caretLoc.lineIndex))
		{
		TERefreshLines(caretLoc.lineIndex, caretLoc.lineIndex);
		}
}

/******************************************************************************
 MoveCaretVert (protected)

	Moves the caret up or down the specified number of lines.

 ******************************************************************************/

void
JTextEditor::MoveCaretVert
	(
	const JInteger deltaLines
	)
{
	assert( itsSelection.IsEmpty() );

	const JSize lineCount  = GetLineCount();
	const JIndex lineIndex = itsCaretLoc.lineIndex;

	JIndex newLineIndex = 0;
	if (deltaLines > 0 && lineIndex + deltaLines <= lineCount)
		{
		newLineIndex = lineIndex + deltaLines;
		}
	else if (deltaLines > 0)
		{
		newLineIndex = lineCount;
		}
	else if (deltaLines < 0 && lineIndex > (JSize) -deltaLines)
		{
		newLineIndex = lineIndex + deltaLines;
		}
	else if (deltaLines < 0)
		{
		newLineIndex = 1;
		}

	if (newLineIndex > 0)
		{
		const JCoordinate saveX = itsCaretX;

		const JPoint pt(itsCaretX, GetLineTop(newLineIndex)+1);
		SetCaretLocation(CalcCaretLocation(pt));

		itsCaretX = saveX;
		}
}

/******************************************************************************
 GetLineForChar

	Returns the line that the specified character is on.  Since the
	array is sorted, we can use a binary search.  We can use kAnyMatch
	because we know the values are unique.

 ******************************************************************************/

JIndex
JTextEditor::GetLineForChar
	(
	const JIndex charIndex
	)
	const
{
	if (charIndex == 0)
		{
		return 1;
		}

	JBoolean found;
	JIndex lineIndex =
		itsLineStarts->SearchSorted1(charIndex, JOrderedSetT::kAnyMatch, &found);
	if (!found)
		{
		lineIndex--;	// wants to insert -after- the value
		}
	return lineIndex;
}

/******************************************************************************
 CalcInsertionFont (private)

	Returns the font to use when inserting at the specified point.

 ******************************************************************************/

JTextEditor::Font
JTextEditor::CalcInsertionFont
	(
	const JIndex charIndex
	)
	const
{
	if (1 < charIndex && charIndex <= itsBuffer->GetLength() &&
		itsBuffer->GetCharacter(charIndex-1) == '\n')
		{
		return itsStyles->GetElement(charIndex);
		}
	else if (charIndex > 1)
		{
		return itsStyles->GetElement(charIndex-1);
		}
	else if (!itsStyles->IsEmpty())
		{
		return itsStyles->GetElement(1);
		}
	else
		{
		return itsDefFont;
		}
}

/******************************************************************************
 GetLineEnd

	Returns the last character on the specified line.

 ******************************************************************************/

JIndex
JTextEditor::GetLineEnd
	(
	const JIndex lineIndex
	)
	const
{
	if (lineIndex < GetLineCount())
		{
		return GetLineStart(lineIndex+1) - 1;
		}
	else
		{
		return itsPrevBufLength;	// consistent with rest of output from Recalc()
		}
}

/******************************************************************************
 GetLineTop

	Returns the top of the specified line.

 ******************************************************************************/

JCoordinate
JTextEditor::GetLineTop
	(
	const JIndex endLineIndex
	)
	const
{
	assert( itsLineGeom->IndexValid(endLineIndex) );

	JIndex lineIndex = 1;
	JCoordinate y    = 0;

	const JSize runCount = itsLineGeom->GetRunCount();
	for (JIndex i=1; i<=runCount && lineIndex < endLineIndex; i++)
		{
		const JSize runLength    = itsLineGeom->GetRunLength(i);
		const LineGeometry& geom = itsLineGeom->GetRunDataRef(i);

		const JIndex newLineIndex = lineIndex + runLength;
		if (newLineIndex <= endLineIndex)
			{
			lineIndex = newLineIndex;
			y        += geom.height * runLength;
			}
		else	// newLineIndex > endLineIndex
			{
			y += geom.height * (endLineIndex - lineIndex);
			break;
			}
		}

	return y;
}

/******************************************************************************
 GetCharLeft (private)

	Returns the starting x coordinate of the specified character.

 ******************************************************************************/

JCoordinate
JTextEditor::GetCharLeft
	(
	const CaretLocation charLoc
	)
	const
{
	const JIndex firstChar = GetLineStart(charLoc.lineIndex);

	JCoordinate x = 0;
	if (charLoc.charIndex > firstChar)
		{
		x = GetStringWidth(firstChar, charLoc.charIndex-1);
		}
	return x;
}

/******************************************************************************
 GetCharWidth (private)

	Returns the width of the specified character.

 ******************************************************************************/

JCoordinate
JTextEditor::GetCharWidth
	(
	const CaretLocation charLoc
	)
	const
{
	const JCharacter c = itsBuffer->GetCharacter(charLoc.charIndex);
	if (c != '\t')
		{
		const Font f = itsStyles->GetElement(charLoc.charIndex);
		return itsFontMgr->GetCharWidth(f.id, f.size, f.style, c);
		}
	else
		{
		return GetTabWidth(charLoc.charIndex, GetCharLeft(charLoc));
		}
}

/******************************************************************************
 GetStringWidth (private)

	Returns the width of the specified character range.

	The second version updates *runIndex,*firstInRun so that they
	are correct for endIndex+1.

 ******************************************************************************/

JCoordinate
JTextEditor::GetStringWidth
	(
	const JIndex startIndex,
	const JIndex endIndex
	)
	const
{
	JIndex runIndex, firstInRun;
	const JBoolean found = itsStyles->FindRun(startIndex, &runIndex, &firstInRun);
	assert( found );

	return GetStringWidth(startIndex, endIndex, &runIndex, &firstInRun);
}

JCoordinate
JTextEditor::GetStringWidth
	(
	const JIndex	origStartIndex,
	const JIndex	endIndex,
	JIndex*			runIndex,
	JIndex*			firstInRun
	)
	const
{
	assert( IndexValid(origStartIndex) );
	assert( IndexValid(endIndex) );

	// preWidth stores the width of the characters preceding origStartIndex
	// on the line containing origStartIndex.  We calculate this -once- when
	// it is first needed.  (i.e. when we hit the first tab character)

	JCoordinate width    = 0;
	JCoordinate preWidth = -1;

	JIndex startIndex = origStartIndex;
	while (startIndex <= endIndex)
		{
		JSize runLength        = itsStyles->GetRunLength(*runIndex);
		const JSize trueRunEnd = *firstInRun + runLength-1;

		runLength -= startIndex - *firstInRun;
		if (startIndex + runLength-1 > endIndex)
			{
			runLength = endIndex - startIndex + 1;
			}

		const Font& f = itsStyles->GetRunDataRef(*runIndex);

		// If there is a tab in the string, we step up to it and take care of
		// the rest in the next iteration.

		JIndex tabIndex;
		if (LocateTab(startIndex, startIndex + runLength-1, &tabIndex))
			{
			runLength = tabIndex - startIndex;
			}
		else
			{
			tabIndex = 0;
			}

		if (runLength > 0)
			{
			width += itsFontMgr->GetStringWidth(f.id, f.size, f.style,
												itsBuffer->GetCString() + startIndex-1,
												runLength);
			}
		if (tabIndex > 0)
			{
			if (preWidth < 0)
				{
				// recursion: max depth 1
				preWidth = GetCharLeft(CalcCaretLocation(origStartIndex));
				assert( preWidth >= 0 );
				}
			width += GetTabWidth(startIndex + runLength, preWidth + width);
			runLength++;
			}

		startIndex += runLength;
		if (startIndex > trueRunEnd)	// move to next style run
			{
			*firstInRun = startIndex;
			(*runIndex)++;
			}
		}

	return width;
}

/******************************************************************************
 LocateTab (private)

	Returns the index of the first tab character, starting from startIndex.
	If no tab is found, returns kFalse, and *tabIndex = 0.

 ******************************************************************************/

JBoolean
JTextEditor::LocateTab
	(
	const JIndex	startIndex,
	const JIndex	endIndex,
	JIndex*			tabIndex
	)
	const
{
	for (JIndex i=startIndex; i<=endIndex; i++)
		{
		if (itsBuffer->GetCharacter(i) == '\t')
			{
			*tabIndex = i;
			return kTrue;
			}
		}

	*tabIndex = 0;
	return kFalse;
}

/******************************************************************************
 GetTabWidth (virtual protected)

	Given the index of the tab character (charIndex) and the horizontal
	position on the line (in pixels) where the tab character starts (x),
	return the width of the tab character.

	This default implementation rounds the location up to the nearest
	multiple of itsDefTabWidth.

 ******************************************************************************/

JCoordinate
JTextEditor::GetTabWidth
	(
	const JIndex		charIndex,
	const JCoordinate	x
	)
	const
{
	const JCoordinate xnew = ((x / itsDefTabWidth) + 1) * itsDefTabWidth;
	return (xnew - x);
}

/******************************************************************************
 RecalcAll (protected)

 ******************************************************************************/

void
JTextEditor::RecalcAll
	(
	const JBoolean needAdjustStyles
	)
{
	if (itsBreakCROnlyFlag)
		{
		itsWidth = 0;
		}
	itsMaxWordWidth = 0;

	itsLineStarts->RemoveAll();
	itsLineStarts->AppendElement(1);

	itsLineGeom->RemoveAll();

	Recalc(CaretLocation(1,1), itsBuffer->GetLength(), kFalse, needAdjustStyles);
}

/******************************************************************************
 Recalc (private)

	Recalculate layout, starting from caretLoc and continuing at least
	through charCount characters.

	*** We assume that nothing above caretLoc.lineIndex needs to be recalculated.
		We also assume that the first line to recalculate already exists.

 ******************************************************************************/

void
JTextEditor::Recalc
	(
	const CaretLocation	origCaretLoc,
	const JSize			origMinCharCount,
	const JBoolean		deletion,
	const JBoolean		needAdjustStyles
	)
{
	JCoordinate maxLineWidth = 0;
	if (itsBreakCROnlyFlag && GetLineCount() == 1)
		{
		itsWidth = 0;
		}

	JCoordinate origY = -1;
	if (!itsLineGeom->IsEmpty())
		{
		origY = GetLineTop(origCaretLoc.lineIndex);
		}

	const JSize bufLength  = itsBuffer->GetLength();

	CaretLocation caretLoc = origCaretLoc;
	JSize minCharCount     = JMax((JSize) 1, origMinCharCount);
	JIndexRange redrawRange;
	if (bufLength > 0 && needAdjustStyles)
		{
		JIndexRange recalcRange(caretLoc.charIndex, caretLoc.charIndex + minCharCount - 1);
		redrawRange = recalcRange;
		AdjustStylesBeforeRecalc(*itsBuffer, itsStyles, &recalcRange, &redrawRange, deletion);
		caretLoc.charIndex = recalcRange.first;
		minCharCount       = recalcRange.GetLength();

		if (recalcRange.Contains(redrawRange))
			{
			redrawRange.SetToNothing();
			}

		assert( caretLoc.charIndex <= origCaretLoc.charIndex );
		assert( caretLoc.charIndex + minCharCount - 1 >=
				origCaretLoc.charIndex + origMinCharCount - 1 );
		assert( itsBuffer->GetLength() == itsStyles->GetElementCount() );

		if (caretLoc.charIndex < origCaretLoc.charIndex)
			{
			while (caretLoc.charIndex < itsLineStarts->GetElement(caretLoc.lineIndex))
				{
				(caretLoc.lineIndex)--;
				}
			}
		}

	if (caretLoc.charIndex == bufLength+1)
		{
		(caretLoc.charIndex)--;
		caretLoc.lineIndex = GetLineForChar(caretLoc.charIndex);
		}

	JIndex firstLineIndex, lastLineIndex;
	if (bufLength > 0)
		{
		Recalc1(bufLength, caretLoc, minCharCount, &maxLineWidth,
				&firstLineIndex, &lastLineIndex);

		if (!redrawRange.IsEmpty())
			{
			firstLineIndex = JMin(firstLineIndex, GetLineForChar(redrawRange.first));
			lastLineIndex  = JMax(lastLineIndex,  GetLineForChar(redrawRange.last));
			}
		}
	else
		{
		itsLineStarts->RemoveAll();
		itsLineStarts->AppendElement(1);

		itsLineGeom->RemoveAll();

		const Font f = CalcInsertionFont(1);
		JCoordinate ascent,descent;
		const JCoordinate h =
			itsFontMgr->GetLineHeight(f.id, f.size, f.style, &ascent, &descent);
		itsLineGeom->AppendElement(LineGeometry(h, ascent));

		firstLineIndex = lastLineIndex = 1;
		}

	// If the caret is visible, recalculate the line index.

	if (itsSelection.IsEmpty())
		{
		const JIndex origLine = itsCaretLoc.lineIndex;
		itsCaretLoc.lineIndex = GetLineForChar(itsCaretLoc.charIndex);
		if (itsCaretLoc.lineIndex != origLine)
			{
			Broadcast(CaretLineChanged(itsCaretLoc.lineIndex));
			}
		}

	// If we only break at newlines, we control our width.

	if (itsBreakCROnlyFlag && maxLineWidth > itsWidth)
		{
		itsWidth = maxLineWidth;
		}

	// If all the lines are the same height, set the scrolling step size to that.

	if (itsLineGeom->GetRunCount() == 1 && !itsIsPrintingFlag)
		{
		TESetVertScrollStep(GetLineHeight(1));
		}
	else if (!itsIsPrintingFlag)
		{
		TESetVertScrollStep(
			itsFontMgr->GetLineHeight(itsDefFont.id, itsDefFont.size, itsDefFont.style));
		}

	// recalculate the height

	const JSize lineCount = GetLineCount();
	JCoordinate newHeight = GetLineTop(lineCount) + GetLineHeight(lineCount);
	if (EndsWithNewline())								// ends with newline
		{
		newHeight += GetEWNHeight();
		}

	JBoolean needRefreshLines = kTrue;
	if (!itsIsPrintingFlag && newHeight != itsHeight)
		{
		TERefresh();	// redraw everything if the line heights changed
		needRefreshLines = kFalse;
		}

	itsHeight = newHeight;

	// notify the derived class of our new size

	if (!itsIsPrintingFlag)
		{
		TESetGUIBounds(itsLeftMarginWidth + itsWidth + kRightMarginWidth, itsHeight, origY);
		}

	// save buffer length for next time

	itsPrevBufLength = bufLength;

	// show the changes

	if (!itsIsPrintingFlag && needRefreshLines)
		{
		TERefreshLines(firstLineIndex, lastLineIndex);
		}
}

/******************************************************************************
 Recalc1 (private)

	Subroutine called by Recalc().

	(firstLineIndex, lastLineIndex) gives the range of lines that had
	to be recalculated.

 ******************************************************************************/

void
JTextEditor::Recalc1
	(
	const JSize			bufLength,
	const CaretLocation	caretLoc,
	const JSize			origMinCharCount,
	JCoordinate*		maxLineWidth,
	JIndex*				firstLineIndex,
	JIndex*				lastLineIndex
	)
{
	JIndex lineIndex = caretLoc.lineIndex;
	if (!itsBreakCROnlyFlag && lineIndex > 1 &&
		caretLoc.charIndex <= bufLength &&
		NoPrevWhitespaceOnLine(*itsBuffer, caretLoc))
		{
		// If we start in the first word on the line, it
		// might actually belong on the previous line.

		lineIndex--;
		}

	JIndex firstChar   = GetLineStart(lineIndex);
	JSize minCharCount = origMinCharCount + (caretLoc.charIndex - firstChar);
	*firstLineIndex    = lineIndex;

	JIndex runIndex, firstInRun;
	const JBoolean found = itsStyles->FindRun(firstChar, &runIndex, &firstInRun);
	assert( found );

	JSize totalCharCount = 0;
	*maxLineWidth        = itsWidth;
	while (1)
		{
		JCoordinate lineWidth;
		const JSize charCount = RecalcLine(bufLength, firstChar, lineIndex, &lineWidth,
										   &runIndex, &firstInRun);
		totalCharCount += charCount;
		if (*maxLineWidth < lineWidth)
			{
			*maxLineWidth = lineWidth;
			}
		const JIndex endChar = firstChar + charCount-1;
		assert( endChar <= bufLength );

		// remove line starts that are further from the end than the new one
		// (we use (bufLength - endChar) so subtraction won't produce negative numbers)

		while (lineIndex < GetLineCount() &&
			   (itsPrevBufLength+1) - GetLineStart(lineIndex+1) >
					bufLength - endChar)
			{
			itsLineStarts->RemoveElement(lineIndex+1);
			itsLineGeom->RemoveElement(lineIndex+1);
			}

		// check if we are done

		if (endChar >= bufLength)
			{
			// We reached the end of the text.

			break;
			}
		else if (totalCharCount >= minCharCount &&
				 lineIndex < GetLineCount() &&
				 itsPrevBufLength - GetLineStart(lineIndex+1) ==
					bufLength - (endChar+1))
			{
			// The rest of the line starts merely shift.

			const JSize lineCount = itsLineStarts->GetElementCount();
			assert( lineCount > lineIndex );
			const long delta = endChar+1 - GetLineStart(lineIndex+1);
			if (delta != 0)
				{
				const JIndex* lineStart = itsLineStarts->GetCArray();
				for (JIndex i=lineIndex+1; i<=lineCount; i++)
					{
					itsLineStarts->SetElement(i, lineStart[i-1] + delta);
					}
				}
			break;
			}

		// insert the new line start

		lineIndex++;
		firstChar += charCount;

		itsLineStarts->InsertElementAtIndex(lineIndex, firstChar);
		itsLineGeom->InsertElementAtIndex(lineIndex, LineGeometry());

		// This catches the case when the new and old line starts
		// are equally far from the end, but we haven't recalculated
		// far enough yet, so the above breakout code didn't trigger.

		if (lineIndex < GetLineCount() &&
			   itsPrevBufLength - GetLineStart(lineIndex+1) ==
					bufLength - (endChar+1))
			{
			itsLineStarts->RemoveElement(lineIndex+1);
			itsLineGeom->RemoveElement(lineIndex+1);
			}
		}

	*lastLineIndex = lineIndex;
}

/******************************************************************************
 RecalcLine (private)

	Recalculate the line starting with firstChar.  Returns the number
	of characters on the line.  Sets the appropriate values in itsLineGeom.
	Sets *lineWidth to the width of the line in pixels.

	If insertLine is kTrue, then this line is new, so we insert a new
	element into itsLineGeom.

	Updates *runIndex,*firstInRun so that they are correct for the character
	beyond the end of the line.

 ******************************************************************************/

JSize
JTextEditor::RecalcLine
	(
	const JSize		bufLength,
	const JIndex	firstCharIndex,
	const JIndex	lineIndex,
	JCoordinate*	lineWidth,
	JIndex*			runIndex,
	JIndex*			firstInRun
	)
{
	JSize charCount = 0;
	*lineWidth      = 0;

	JIndex gswRunIndex   = *runIndex;
	JIndex gswFirstInRun = *firstInRun;

	if (itsBreakCROnlyFlag)
		{
		JIndex endIndex = firstCharIndex;
		if (!itsBuffer->LocateNextSubstring("\n", &endIndex))
			{
			endIndex = itsBuffer->GetLength();
			}
		charCount  = endIndex - firstCharIndex + 1;
		*lineWidth = GetStringWidth(firstCharIndex, endIndex,
									&gswRunIndex, &gswFirstInRun);
		}

	else
		{
		// include leading whitespace

		JBoolean endOfLine;
		charCount = IncludeWhitespaceOnLine(bufLength, firstCharIndex,
											lineWidth, &endOfLine,
											&gswRunIndex, &gswFirstInRun);
		JIndex charIndex = firstCharIndex + charCount;

		// Add words until we hit the right margin, a newline,
		// or the end of the buffer.

		while (charIndex <= bufLength && !endOfLine)
			{
			// get the next word

			JIndex prevIndex = charIndex;
			if (!LocateNextWhitespace(bufLength, &charIndex))
				{
				charIndex = bufLength+1;
				}

			// check if the word fits on this line

			JCoordinate dw =
				GetStringWidth(prevIndex, charIndex-1, &gswRunIndex, &gswFirstInRun);
			if (itsMaxWordWidth < dw)
				{
				itsMaxWordWidth = dw;
				}
			if (*lineWidth + dw > itsWidth)
				{
				if (prevIndex != firstCharIndex)
					{
					// this word goes on the next line

					charIndex = prevIndex;
					}
				else
					{
					// put as much of this word as possible on the line

					assert( *lineWidth == 0 && charCount == 0 );
					charCount = GetSubwordForLine(bufLength, lineIndex,
												  firstCharIndex, lineWidth);
					}
				break;
				}

			// put the word on this line

			*lineWidth += dw;
			charCount  += charIndex - prevIndex;

			// include the whitespace after the word

			JSize wsCount =
				IncludeWhitespaceOnLine(bufLength, charIndex, lineWidth, &endOfLine,
										&gswRunIndex, &gswFirstInRun);
			charIndex += wsCount;
			charCount += wsCount;
			}
		}

	// update geometry for this line

	const JSize runCount = itsStyles->GetRunCount();
	const JSize lastChar = firstCharIndex + charCount-1;

	JCoordinate maxAscent=0, maxDescent=0;
	while (*runIndex <= runCount)
		{
		const Font& f = itsStyles->GetRunDataRef(*runIndex);
		JCoordinate ascent, descent;
		itsFontMgr->GetLineHeight(f.id, f.size, f.style, &ascent, &descent);

		if (ascent > maxAscent)
			{
			maxAscent = ascent;
			}
		if (descent > maxDescent)
			{
			maxDescent = descent;
			}

		const JIndex firstInNextRun = *firstInRun + itsStyles->GetRunLength(*runIndex);
		if (firstInNextRun <= lastChar+1)
			{
			(*runIndex)++;
			*firstInRun = firstInNextRun;
			}
		if (firstInNextRun > lastChar)
			{
			break;
			}
		}

	const LineGeometry geom(maxAscent+maxDescent, maxAscent);
	if (lineIndex <= itsLineGeom->GetElementCount())
		{
		itsLineGeom->SetElement(lineIndex, geom);
		}
	else
		{
		itsLineGeom->AppendElement(geom);
		}

	// return number of characters on line

	return charCount;
}

/******************************************************************************
 IncludeWhitespaceOnLine (private)

	*** Only for use by RecalcLine()

	Starting with the given index, return the number of consecutive whitespace
	characters encountered.  Increments *lineWidth with the width of this
	whitespace.  If we encounter a newline, we stop beyond it and set
	*endOfLine to kTrue.

	Updates *runIndex,*firstInRun so that they are correct for the character
	beyond the end of the whitespace.

 ******************************************************************************/

JSize
JTextEditor::IncludeWhitespaceOnLine
	(
	const JSize		bufLength,
	const JIndex	origStartIndex,
	JCoordinate*	lineWidth,
	JBoolean*		endOfLine,
	JIndex*			runIndex,
	JIndex*			firstInRun
	)
	const
{
	*endOfLine = kFalse;

	JIndex startIndex = origStartIndex;
	JIndex endIndex   = startIndex;
	JSize wsCount     = 0;

	while (endIndex <= bufLength)
		{
		const JCharacter c = itsBuffer->GetCharacter(endIndex);
		if (!isspace(c))
			{
			break;
			}

		wsCount++;
		if (c == '\t')
			{
			if (endIndex > startIndex)
				{
				*lineWidth += GetStringWidth(startIndex, endIndex-1,
											 runIndex, firstInRun);
				}
			*lineWidth += GetTabWidth(endIndex, *lineWidth);
			startIndex  = endIndex+1;

			// update *runIndex,*firstInRun after passing tab character

			const JSize runLength = itsStyles->GetRunLength(*runIndex);
			if (startIndex > *firstInRun + runLength-1)
				{
				*firstInRun += runLength;
				(*runIndex)++;
				}

			// tab characters can wrap to the next line

			if (!itsBreakCROnlyFlag && *lineWidth > itsWidth)
				{
				*endOfLine = kTrue;
				break;
				}
			}

		endIndex++;
		if (c == '\n')
			{
			*endOfLine = kTrue;
			break;
			}
		}

	if (endIndex > startIndex)
		{
		*lineWidth += GetStringWidth(startIndex, endIndex-1, runIndex, firstInRun);
		}

	return wsCount;
}

/******************************************************************************
 LocateNextWhitespace (private)

	*** Only for use by RecalcLine()

	Find the next whitespace character, starting from the given index.
	Returns kFalse if it doesn't find any.

	*** If we only break at newlines, only newlines are considered to be
		whitespace.

 ******************************************************************************/

JBoolean
JTextEditor::LocateNextWhitespace
	(
	const JSize	bufLength,
	JIndex*		startIndex
	)
	const
{
	for (JIndex i=*startIndex; i<bufLength; i++)
		{
		const JCharacter c = itsBuffer->GetCharacter(i);
		if ((!itsBreakCROnlyFlag && isspace(c)) ||
			( itsBreakCROnlyFlag && c == '\n'))
			{
			*startIndex = i;
			return kTrue;
			}
		}

	return kFalse;
}

/******************************************************************************
 GetSubwordForLine (private)

	*** Only for use by RecalcLine()

	Starting with the given index, return the number of characters that
	will fit on a line.  Sets *lineWidth to the width of the line in pixels.
	We always put at least one character on the line.

 ******************************************************************************/

JSize
JTextEditor::GetSubwordForLine
	(
	const JSize		bufLength,
	const JIndex	lineIndex,
	const JIndex	startIndex,
	JCoordinate*	lineWidth
	)
	const
{
	*lineWidth = 0;

	JIndex endIndex = 0;
	CaretLocation caretLoc(0, lineIndex);
	for (JIndex i=startIndex; i<=bufLength; i++)
		{
		caretLoc.charIndex   = i;
		const JCoordinate dw = GetCharWidth(caretLoc);
		if (i > startIndex && *lineWidth + dw > itsWidth)
			{
			endIndex = i-1;
			break;
			}
		else
			{
			*lineWidth += dw;
			}
		}
	if (endIndex == 0)
		{
		endIndex = bufLength;
		}

	return (endIndex - startIndex + 1);
}

/******************************************************************************
 NoPrevWhitespaceOnLine (private)

	Returns kTrue if there is no whitespace between startIndex-1 and
	the first character on the line.

	Called by Recalc1() to decide whether or not to start on the
	previous line.

 ******************************************************************************/

JBoolean
JTextEditor::NoPrevWhitespaceOnLine
	(
	const JCharacter*	str,
	const CaretLocation	startLoc
	)
	const
{
	const JIndex firstChar = GetLineStart(startLoc.lineIndex);
	for (JIndex i = startLoc.charIndex - 1; i>firstChar; i--)
		{
		if (isspace(str[i-1]))
			{
			return kFalse;
			}
		}

	return kTrue;	// we hit the start of the string
}

/******************************************************************************
 AdjustStylesBeforeRecalc (virtual protected)

	Called before Recalc() begins its work.  Derived classes can override
	this to adjust the style of the affected range of text.  range is an
	in/out variable because the changes usually slop out beyond the initial
	range.

	*** The endpoints of range are only allowed to move outward.

	*** The contents of *styles can change, but the length must remain
		the same.  *range must be expanded to include all the changed
		elements.

 ******************************************************************************/

void
JTextEditor::AdjustStylesBeforeRecalc
	(
	const JString&		buffer,
	JRunArray<Font>*	styles,
	JIndexRange*		recalcRange,
	JIndexRange*		redrawRange,
	const JBoolean		deletion
	)
{
}

/******************************************************************************
 CalcCaretLocation (protected)

	Return the closest insertion point.  If the line ends with a space,
	the last possible insertion point is in front of this space

 ******************************************************************************/

JTextEditor::CaretLocation
JTextEditor::CalcCaretLocation
	(
	const JPoint& pt
	)
	const
{
	const JSize bufLength = itsBuffer->GetLength();
	if (bufLength == 0)
		{
		return CaretLocation(1,1);
		}
	else if (pt.y >= itsHeight)
		{
		return CaretLocation(bufLength+1, GetLineCount());
		}

	// find the line that was clicked on

	JCoordinate lineTop;
	const JIndex lineIndex = CalcLineIndex(pt.y, &lineTop);
	if (EndsWithNewline() &&									// ends with newline
		itsHeight - GetEWNHeight() < pt.y && pt.y <= itsHeight)
		{
		return CaretLocation(bufLength+1, GetLineCount());
		}

	// find the closest insertion point

	JIndex charIndex = 0;
	{
	const JIndex lineStart = GetLineStart(lineIndex);

	JIndex lineEnd = GetLineEnd(lineIndex);
	if ((lineEnd < bufLength || EndsWithNewline()) &&	// ends with newline
		isspace(itsBuffer->GetCharacter(lineEnd)))
		{
		lineEnd--;
		}

	JCoordinate prevD = pt.x;
	if (prevD <= 0)
		{
		charIndex = lineStart;
		}
	else
		{
		JCoordinate x = 0;
		for (JIndex i=lineStart; i<=lineEnd; i++)
			{
			x += GetCharWidth(CaretLocation(i, lineIndex));
			const JCoordinate d = pt.x - x;
			if (d == 0)
				{
				charIndex = i+1;
				break;
				}
			else if (d < 0 && prevD <= -d)
				{
				charIndex = i;
				break;
				}
			else if (d < 0 && prevD > -d)
				{
				charIndex = i+1;
				break;
				}
			prevD = d;
			}

		if (charIndex == 0)
			{
			charIndex = lineEnd+1;
			}
		}
	}

	return CaretLocation(charIndex, lineIndex);
}

/******************************************************************************
 CalcLineIndex (private)

 ******************************************************************************/

JIndex
JTextEditor::CalcLineIndex
	(
	const JCoordinate	y,
	JCoordinate*		lineTop
	)
	const
{
	JIndex lineIndex = 1;
	JCoordinate h    = 0;

	const JSize runCount = itsLineGeom->GetRunCount();
	for (JIndex i=1; i<=runCount && h < y; i++)
		{
		const JSize runLength    = itsLineGeom->GetRunLength(i);
		const LineGeometry& geom = itsLineGeom->GetRunDataRef(i);

		const JCoordinate newH = h + geom.height * runLength;
		if (y < newH)
			{
			const JSize deltaLines = (y - h) / geom.height;
			lineIndex += deltaLines;
			h         += geom.height * deltaLines;
			break;
			}
		else
			{
			lineIndex += runLength;
			h          = newH;

			if (i == runCount)		// return last line
				{
				lineIndex--;
				h -= geom.height;
				}
			}
		}

	*lineTop = h;
	return lineIndex;
}

/******************************************************************************
 GetEWNHeight (private)

	Not inline to avoid including JFontManager.h in header file

 ******************************************************************************/

JCoordinate
JTextEditor::GetEWNHeight()
	const
{
	const Font f = CalcInsertionFont(itsBuffer->GetLength() + 1);
	return itsFontMgr->GetLineHeight(f.id, f.size, f.style);
}

#define JTemplateType JTextEditor::Font
#include <JArray.tmpl>
#include <JRunArray.tmpls>
#undef JTemplateType

#define JTemplateType JArray<JTextEditor::Font>,JTextEditor::Font
#include <JStack.tmpls>
#undef JTemplateType

#define JTemplateType JTextEditor::LineGeometry
#include <JRunArray.tmpls>
#undef JTemplateType

#define JTemplateType JTEUndoBase
#include <JPtrArray.tmpls>
#undef JTemplateType

#define JTemplateType JTextEditor::CRMRule
#include <JArray.tmpls>
#undef JTemplateType
