bool update (CodeDocument& codeDoc, int lineNum,
                 CodeDocument::Iterator& source,
                 CodeTokeniser* tokeniser, const int tabSpaces,
                 const CodeDocument::Position& selStart,
                 const CodeDocument::Position& selEnd)
    {
        Array <SyntaxToken> newTokens;
        newTokens.ensureStorageAllocated (8);

        if (tokeniser == nullptr)
        {
            const String line (codeDoc.getLine (lineNum));
            addToken (newTokens, line, line.length(), -1);
        }
        else if (lineNum < codeDoc.getNumLines())
        {
            const CodeDocument::Position pos (codeDoc, lineNum, 0);
            createTokens (pos.getPosition(), pos.getLineText(),
                          source, *tokeniser, newTokens);
        }

        replaceTabsWithSpaces (newTokens, tabSpaces);

        int newHighlightStart = 0;
        int newHighlightEnd = 0;

        if (selStart.getLineNumber() <= lineNum && selEnd.getLineNumber() >= lineNum)
        {
            const String line (codeDoc.getLine (lineNum));

            CodeDocument::Position lineStart (codeDoc, lineNum, 0), lineEnd (codeDoc, lineNum + 1, 0);
            newHighlightStart = indexToColumn (jmax (0, selStart.getPosition() - lineStart.getPosition()),
                                               line, tabSpaces);
            newHighlightEnd = indexToColumn (jmin (lineEnd.getPosition() - lineStart.getPosition(), selEnd.getPosition() - lineStart.getPosition()),
                                             line, tabSpaces);
        }

        if (newHighlightStart != highlightColumnStart || newHighlightEnd != highlightColumnEnd)
        {
            highlightColumnStart = newHighlightStart;
            highlightColumnEnd = newHighlightEnd;
        }
        else if (tokens == newTokens)
        {
            return false;
        }

        tokens.swapWith (newTokens);
        return true;
    }
const Rectangle CodeEditorComponent::getCharacterBounds (const CodeDocument::Position& pos) const throw()
{
    return Rectangle (roundToInt ((gutter - xOffset * charWidth) + indexToColumn (pos.getLineNumber(), pos.getIndexInLine()) * charWidth),
                      (pos.getLineNumber() - firstLineOnScreen) * lineHeight,
                      roundToInt (charWidth),
                      lineHeight);
}
bool CodeEditorComponent::deleteWhitespaceBackwardsToTabStop()
{
    if (getHighlightedRegion().isEmpty())
    {
        for (;;)
        {
            const int currentColumn = indexToColumn (caretPos.getLineNumber(), caretPos.getIndexInLine());

            if (currentColumn <= 0 || (currentColumn % spacesPerTab) == 0)
                break;

            moveCaretLeft (false, true);
        }

        const String selected (getTextInRange (getHighlightedRegion()));

        if (selected.isNotEmpty() && selected.trim().isEmpty())
        {
            cut();
            return true;
        }
    }

    return false;
}
void CodeEditorComponent::scrollToKeepCaretOnScreen()
{
    if (caretPos.getLineNumber() < firstLineOnScreen)
        scrollBy (caretPos.getLineNumber() - firstLineOnScreen);
    else if (caretPos.getLineNumber() >= firstLineOnScreen + linesOnScreen)
        scrollBy (caretPos.getLineNumber() - (firstLineOnScreen + linesOnScreen - 1));

    const int column = indexToColumn (caretPos.getLineNumber(), caretPos.getIndexInLine());
    if (column >= xOffset + columnsOnScreen - 1)
        scrollToColumn (column + 1 - columnsOnScreen);
    else if (column < xOffset)
        scrollToColumn (column);
}
void CodeEditorComponent::moveLineDelta (const int delta, const bool selecting)
{
    CodeDocument::Position pos (caretPos);
    const int newLineNum = pos.getLineNumber() + delta;

    if (columnToTryToMaintain < 0)
        columnToTryToMaintain = indexToColumn (pos.getLineNumber(), pos.getIndexInLine());

    pos.setLineAndIndex (newLineNum, columnToIndex (newLineNum, columnToTryToMaintain));

    const int colToMaintain = columnToTryToMaintain;
    moveCaretTo (pos, selecting);
    columnToTryToMaintain = colToMaintain;
}
void CodeEditorComponent::scrollToKeepCaretOnScreen()
{
    if (getWidth() > 0 && getHeight() > 0)
    {
        const int caretLine = caretPos.getLineNumber();
        scrollToKeepLinesOnScreen (Range<int> (caretLine, caretLine));

        const int column = indexToColumn (caretPos.getLineNumber(), caretPos.getIndexInLine());
        if (column >= xOffset + columnsOnScreen - 1)
            scrollToColumn (column + 1 - columnsOnScreen);
        else if (column < xOffset)
            scrollToColumn (column);
    }
}
void CodeEditorComponent::insertTabAtCaret()
{
    if (CharacterFunctions::isWhitespace (caretPos.getCharacter())
         && caretPos.getLineNumber() == caretPos.movedBy (1).getLineNumber())
    {
        moveCaretTo (document.findWordBreakAfter (caretPos), false);
    }

    if (useSpacesForTabs)
    {
        const int caretCol = indexToColumn (caretPos.getLineNumber(), caretPos.getIndexInLine());
        const int spacesNeeded = spacesPerTab - (caretCol % spacesPerTab);
        insertTextAtCaret (String::repeatedString (T(" "), spacesNeeded));
    }
    else
    {
        insertTextAtCaret (T("\t"));
    }
}
bool CodeEditorComponent::skipBackwardsToPreviousTab()
{
    const String currentLineText (caretPos.getLineText().removeCharacters ("\r\n"));
    const int currentIndex = caretPos.getIndexInLine();

    if (currentLineText.isNotEmpty() && currentLineText.length() == currentIndex)
    {
        const int currentLine = caretPos.getLineNumber();
        const int currentColumn = indexToColumn (currentLine, currentIndex);
        const int previousTabColumn = (currentColumn - 1) - ((currentColumn - 1) % spacesPerTab);
        const int previousTabIndex = columnToIndex (currentLine, previousTabColumn);

        if (currentLineText.substring (previousTabIndex, currentIndex).trim().isEmpty())
        {
            selectionStart.moveBy (previousTabIndex - currentIndex);
            return true;
        }
    }

    return false;
}
void CodeEditorComponent::indentSelectedLines (const int spacesToAdd)
{
    newTransaction();

    CodeDocument::Position oldSelectionStart (selectionStart), oldSelectionEnd (selectionEnd), oldCaret (caretPos);
    oldSelectionStart.setPositionMaintained (true);
    oldSelectionEnd.setPositionMaintained (true);
    oldCaret.setPositionMaintained (true);

    const int lineStart = selectionStart.getLineNumber();
    int lineEnd = selectionEnd.getLineNumber();

    if (lineEnd > lineStart && selectionEnd.getIndexInLine() == 0)
        --lineEnd;

    for (int line = lineStart; line <= lineEnd; ++line)
    {
        const String lineText (document.getLine (line));
        const int nonWhitespaceStart = CodeEditorHelpers::findFirstNonWhitespaceChar (lineText);

        if (nonWhitespaceStart > 0 || lineText.trimStart().isNotEmpty())
        {
            const CodeDocument::Position wsStart (document, line, 0);
            const CodeDocument::Position wsEnd   (document, line, nonWhitespaceStart);

            const int numLeadingSpaces = indexToColumn (line, wsEnd.getIndexInLine());
            const int newNumLeadingSpaces = jmax (0, numLeadingSpaces + spacesToAdd);

            if (newNumLeadingSpaces != numLeadingSpaces)
            {
                document.deleteSection (wsStart, wsEnd);
                document.insertText (wsStart, getTabString (newNumLeadingSpaces));
            }
        }
    }

    selectionStart = oldSelectionStart;
    selectionEnd = oldSelectionEnd;
    caretPos = oldCaret;
}
    bool update (CodeDocument& document, int lineNum,
                 CodeDocument::Iterator& source,
                 CodeTokeniser* analyser, const int spacesPerTab,
                 const CodeDocument::Position& selectionStart,
                 const CodeDocument::Position& selectionEnd)
    {
        OwnedArray <SyntaxToken> newTokens;

        if (analyser == 0)
        {
            newTokens.add (new SyntaxToken (document.getLine (lineNum), -1));
        }
        else if (lineNum < document.getNumLines())
        {
            const CodeDocument::Position pos (&document, lineNum, 0);
            createTokens (pos.getPosition(), pos.getLineText(),
                          source, analyser, newTokens);
        }

        replaceTabsWithSpaces (newTokens, spacesPerTab);

        int newHighlightStart = 0;
        int newHighlightEnd = 0;

        if (selectionStart.getLineNumber() <= lineNum && selectionEnd.getLineNumber() >= lineNum)
        {
            const String line (document.getLine (lineNum));

            CodeDocument::Position lineStart (&document, lineNum, 0), lineEnd (&document, lineNum + 1, 0);
            newHighlightStart = indexToColumn (jmax (0, selectionStart.getPosition() - lineStart.getPosition()),
                                               line, spacesPerTab);
            newHighlightEnd = indexToColumn (jmin (lineEnd.getPosition() - lineStart.getPosition(), selectionEnd.getPosition() - lineStart.getPosition()),
                                             line, spacesPerTab);
        }

        if (newHighlightStart != highlightColumnStart || newHighlightEnd != highlightColumnEnd)
        {
            highlightColumnStart = newHighlightStart;
            highlightColumnEnd = newHighlightEnd;
        }
        else
        {
            if (tokens.size() == newTokens.size())
            {
                bool allTheSame = true;

                for (int i = newTokens.size(); --i >= 0;)
                {
                    if (*tokens.getUnchecked(i) != *newTokens.getUnchecked(i))
                    {
                        allTheSame = false;
                        break;
                    }
                }

                if (allTheSame)
                    return false;
            }
        }

        tokens.swapWithArray (newTokens);
        return true;
    }