|
- /*
- ==============================================================================
-
- This file is part of the JUCE library.
- Copyright (c) 2020 - Raw Material Software Limited
-
- JUCE is an open source library subject to commercial or open-source
- licensing.
-
- By using JUCE, you agree to the terms of both the JUCE 6 End-User License
- Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
-
- End User License Agreement: www.juce.com/juce-6-licence
- Privacy Policy: www.juce.com/juce-privacy-policy
-
- Or: You may also use this code under the terms of the GPL v3 (see
- www.gnu.org/licenses).
-
- JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
- EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
- DISCLAIMED.
-
- ==============================================================================
- */
-
- namespace juce
- {
-
- class CodeDocumentLine
- {
- public:
- CodeDocumentLine (const String::CharPointerType startOfLine,
- const String::CharPointerType endOfLine,
- const int lineLen,
- const int numNewLineChars,
- const int startInFile)
- : line (startOfLine, endOfLine),
- lineStartInFile (startInFile),
- lineLength (lineLen),
- lineLengthWithoutNewLines (lineLen - numNewLineChars)
- {
- }
-
- static void createLines (Array<CodeDocumentLine*>& newLines, StringRef text)
- {
- auto t = text.text;
- int charNumInFile = 0;
- bool finished = false;
-
- while (! (finished || t.isEmpty()))
- {
- auto startOfLine = t;
- auto startOfLineInFile = charNumInFile;
- int lineLength = 0;
- int numNewLineChars = 0;
-
- for (;;)
- {
- auto c = t.getAndAdvance();
-
- if (c == 0)
- {
- finished = true;
- break;
- }
-
- ++charNumInFile;
- ++lineLength;
-
- if (c == '\r')
- {
- ++numNewLineChars;
-
- if (*t == '\n')
- {
- ++t;
- ++charNumInFile;
- ++lineLength;
- ++numNewLineChars;
- }
-
- break;
- }
-
- if (c == '\n')
- {
- ++numNewLineChars;
- break;
- }
- }
-
- newLines.add (new CodeDocumentLine (startOfLine, t, lineLength,
- numNewLineChars, startOfLineInFile));
- }
-
- jassert (charNumInFile == text.length());
- }
-
- bool endsWithLineBreak() const noexcept
- {
- return lineLengthWithoutNewLines != lineLength;
- }
-
- void updateLength() noexcept
- {
- lineLength = 0;
- lineLengthWithoutNewLines = 0;
-
- for (auto t = line.getCharPointer();;)
- {
- auto c = t.getAndAdvance();
-
- if (c == 0)
- break;
-
- ++lineLength;
-
- if (c != '\n' && c != '\r')
- lineLengthWithoutNewLines = lineLength;
- }
- }
-
- String line;
- int lineStartInFile, lineLength, lineLengthWithoutNewLines;
- };
-
- //==============================================================================
- CodeDocument::Iterator::Iterator (const CodeDocument& doc) noexcept
- : document (&doc)
- {}
-
- CodeDocument::Iterator::Iterator (CodeDocument::Position p) noexcept
- : document (p.owner),
- line (p.getLineNumber()),
- position (p.getPosition())
- {
- reinitialiseCharPtr();
-
- for (int i = 0; i < p.getIndexInLine(); ++i)
- {
- charPointer.getAndAdvance();
-
- if (charPointer.isEmpty())
- {
- position -= (p.getIndexInLine() - i);
- break;
- }
- }
- }
-
- CodeDocument::Iterator::Iterator() noexcept
- : document (nullptr)
- {
- }
-
- CodeDocument::Iterator::~Iterator() noexcept {}
-
-
- bool CodeDocument::Iterator::reinitialiseCharPtr() const
- {
- /** You're trying to use a default constructed iterator. Bad idea! */
- jassert (document != nullptr);
-
- if (charPointer.getAddress() == nullptr)
- {
- if (auto* l = document->lines[line])
- charPointer = l->line.getCharPointer();
- else
- return false;
- }
-
- return true;
- }
-
- juce_wchar CodeDocument::Iterator::nextChar() noexcept
- {
- for (;;)
- {
- if (! reinitialiseCharPtr())
- return 0;
-
- if (auto result = charPointer.getAndAdvance())
- {
- if (charPointer.isEmpty())
- {
- ++line;
- charPointer = nullptr;
- }
-
- ++position;
- return result;
- }
-
- ++line;
- charPointer = nullptr;
- }
- }
-
- void CodeDocument::Iterator::skip() noexcept
- {
- nextChar();
- }
-
- void CodeDocument::Iterator::skipToEndOfLine() noexcept
- {
- if (! reinitialiseCharPtr())
- return;
-
- position += (int) charPointer.length();
- ++line;
- charPointer = nullptr;
- }
-
- void CodeDocument::Iterator::skipToStartOfLine() noexcept
- {
- if (! reinitialiseCharPtr())
- return;
-
- if (auto* l = document->lines [line])
- {
- auto startPtr = l->line.getCharPointer();
- position -= (int) startPtr.lengthUpTo (charPointer);
- charPointer = startPtr;
- }
- }
-
- juce_wchar CodeDocument::Iterator::peekNextChar() const noexcept
- {
- if (! reinitialiseCharPtr())
- return 0;
-
- if (auto c = *charPointer)
- return c;
-
- if (auto* l = document->lines [line + 1])
- return l->line[0];
-
- return 0;
- }
-
- juce_wchar CodeDocument::Iterator::previousChar() noexcept
- {
- if (! reinitialiseCharPtr())
- return 0;
-
- for (;;)
- {
- if (auto* l = document->lines[line])
- {
- if (charPointer != l->line.getCharPointer())
- {
- --position;
- --charPointer;
- break;
- }
- }
-
- if (line == 0)
- return 0;
-
- --line;
-
- if (auto* prev = document->lines[line])
- charPointer = prev->line.getCharPointer().findTerminatingNull();
- }
-
- return *charPointer;
- }
-
- juce_wchar CodeDocument::Iterator::peekPreviousChar() const noexcept
- {
- if (! reinitialiseCharPtr())
- return 0;
-
- if (auto* l = document->lines[line])
- {
- if (charPointer != l->line.getCharPointer())
- return *(charPointer - 1);
-
- if (auto* prev = document->lines[line - 1])
- return *(prev->line.getCharPointer().findTerminatingNull() - 1);
- }
-
- return 0;
- }
-
- void CodeDocument::Iterator::skipWhitespace() noexcept
- {
- while (CharacterFunctions::isWhitespace (peekNextChar()))
- skip();
- }
-
- bool CodeDocument::Iterator::isEOF() const noexcept
- {
- return charPointer.getAddress() == nullptr && line >= document->lines.size();
- }
-
- bool CodeDocument::Iterator::isSOF() const noexcept
- {
- return position == 0;
- }
-
- CodeDocument::Position CodeDocument::Iterator::toPosition() const
- {
- if (auto* l = document->lines[line])
- {
- reinitialiseCharPtr();
- int indexInLine = 0;
- auto linePtr = l->line.getCharPointer();
-
- while (linePtr != charPointer && ! linePtr.isEmpty())
- {
- ++indexInLine;
- ++linePtr;
- }
-
- return CodeDocument::Position (*document, line, indexInLine);
- }
-
- if (isEOF())
- {
- if (auto* last = document->lines.getLast())
- {
- auto lineIndex = document->lines.size() - 1;
- return CodeDocument::Position (*document, lineIndex, last->lineLength);
- }
- }
-
- return CodeDocument::Position (*document, 0, 0);
- }
-
- //==============================================================================
- CodeDocument::Position::Position() noexcept
- {
- }
-
- CodeDocument::Position::Position (const CodeDocument& ownerDocument,
- const int lineNum, const int index) noexcept
- : owner (const_cast<CodeDocument*> (&ownerDocument)),
- line (lineNum), indexInLine (index)
- {
- setLineAndIndex (lineNum, index);
- }
-
- CodeDocument::Position::Position (const CodeDocument& ownerDocument, int pos) noexcept
- : owner (const_cast<CodeDocument*> (&ownerDocument))
- {
- setPosition (pos);
- }
-
- CodeDocument::Position::Position (const Position& other) noexcept
- : owner (other.owner), characterPos (other.characterPos), line (other.line),
- indexInLine (other.indexInLine)
- {
- jassert (*this == other);
- }
-
- CodeDocument::Position::~Position()
- {
- setPositionMaintained (false);
- }
-
- CodeDocument::Position& CodeDocument::Position::operator= (const Position& other)
- {
- if (this != &other)
- {
- const bool wasPositionMaintained = positionMaintained;
- if (owner != other.owner)
- setPositionMaintained (false);
-
- owner = other.owner;
- line = other.line;
- indexInLine = other.indexInLine;
- characterPos = other.characterPos;
- setPositionMaintained (wasPositionMaintained);
-
- jassert (*this == other);
- }
-
- return *this;
- }
-
- bool CodeDocument::Position::operator== (const Position& other) const noexcept
- {
- jassert ((characterPos == other.characterPos)
- == (line == other.line && indexInLine == other.indexInLine));
-
- return characterPos == other.characterPos
- && line == other.line
- && indexInLine == other.indexInLine
- && owner == other.owner;
- }
-
- bool CodeDocument::Position::operator!= (const Position& other) const noexcept
- {
- return ! operator== (other);
- }
-
- void CodeDocument::Position::setLineAndIndex (const int newLineNum, const int newIndexInLine)
- {
- jassert (owner != nullptr);
-
- if (owner->lines.size() == 0)
- {
- line = 0;
- indexInLine = 0;
- characterPos = 0;
- }
- else
- {
- if (newLineNum >= owner->lines.size())
- {
- line = owner->lines.size() - 1;
-
- auto& l = *owner->lines.getUnchecked (line);
- indexInLine = l.lineLengthWithoutNewLines;
- characterPos = l.lineStartInFile + indexInLine;
- }
- else
- {
- line = jmax (0, newLineNum);
-
- auto& l = *owner->lines.getUnchecked (line);
-
- if (l.lineLengthWithoutNewLines > 0)
- indexInLine = jlimit (0, l.lineLengthWithoutNewLines, newIndexInLine);
- else
- indexInLine = 0;
-
- characterPos = l.lineStartInFile + indexInLine;
- }
- }
- }
-
- void CodeDocument::Position::setPosition (const int newPosition)
- {
- jassert (owner != nullptr);
-
- line = 0;
- indexInLine = 0;
- characterPos = 0;
-
- if (newPosition > 0)
- {
- int lineStart = 0;
- auto lineEnd = owner->lines.size();
-
- for (;;)
- {
- if (lineEnd - lineStart < 4)
- {
- for (int i = lineStart; i < lineEnd; ++i)
- {
- auto& l = *owner->lines.getUnchecked (i);
- auto index = newPosition - l.lineStartInFile;
-
- if (index >= 0 && (index < l.lineLength || i == lineEnd - 1))
- {
- line = i;
- indexInLine = jmin (l.lineLengthWithoutNewLines, index);
- characterPos = l.lineStartInFile + indexInLine;
- }
- }
-
- break;
- }
- else
- {
- auto midIndex = (lineStart + lineEnd + 1) / 2;
-
- if (newPosition >= owner->lines.getUnchecked (midIndex)->lineStartInFile)
- lineStart = midIndex;
- else
- lineEnd = midIndex;
- }
- }
- }
- }
-
- void CodeDocument::Position::moveBy (int characterDelta)
- {
- jassert (owner != nullptr);
-
- if (characterDelta == 1)
- {
- setPosition (getPosition());
-
- // If moving right, make sure we don't get stuck between the \r and \n characters..
- if (line < owner->lines.size())
- {
- auto& l = *owner->lines.getUnchecked (line);
-
- if (indexInLine + characterDelta < l.lineLength
- && indexInLine + characterDelta >= l.lineLengthWithoutNewLines + 1)
- ++characterDelta;
- }
- }
-
- setPosition (characterPos + characterDelta);
- }
-
- CodeDocument::Position CodeDocument::Position::movedBy (const int characterDelta) const
- {
- CodeDocument::Position p (*this);
- p.moveBy (characterDelta);
- return p;
- }
-
- CodeDocument::Position CodeDocument::Position::movedByLines (const int deltaLines) const
- {
- CodeDocument::Position p (*this);
- p.setLineAndIndex (getLineNumber() + deltaLines, getIndexInLine());
- return p;
- }
-
- juce_wchar CodeDocument::Position::getCharacter() const
- {
- if (auto* l = owner->lines [line])
- return l->line [getIndexInLine()];
-
- return 0;
- }
-
- String CodeDocument::Position::getLineText() const
- {
- if (auto* l = owner->lines [line])
- return l->line;
-
- return {};
- }
-
- void CodeDocument::Position::setPositionMaintained (const bool isMaintained)
- {
- if (isMaintained != positionMaintained)
- {
- positionMaintained = isMaintained;
-
- if (owner != nullptr)
- {
- if (isMaintained)
- {
- jassert (! owner->positionsToMaintain.contains (this));
- owner->positionsToMaintain.add (this);
- }
- else
- {
- // If this happens, you may have deleted the document while there are Position objects that are still using it...
- jassert (owner->positionsToMaintain.contains (this));
- owner->positionsToMaintain.removeFirstMatchingValue (this);
- }
- }
- }
- }
-
- //==============================================================================
- CodeDocument::CodeDocument() : undoManager (std::numeric_limits<int>::max(), 10000)
- {
- }
-
- CodeDocument::~CodeDocument()
- {
- }
-
- String CodeDocument::getAllContent() const
- {
- return getTextBetween (Position (*this, 0),
- Position (*this, lines.size(), 0));
- }
-
- String CodeDocument::getTextBetween (const Position& start, const Position& end) const
- {
- if (end.getPosition() <= start.getPosition())
- return {};
-
- auto startLine = start.getLineNumber();
- auto endLine = end.getLineNumber();
-
- if (startLine == endLine)
- {
- if (auto* line = lines [startLine])
- return line->line.substring (start.getIndexInLine(), end.getIndexInLine());
-
- return {};
- }
-
- MemoryOutputStream mo;
- mo.preallocate ((size_t) (end.getPosition() - start.getPosition() + 4));
-
- auto maxLine = jmin (lines.size() - 1, endLine);
-
- for (int i = jmax (0, startLine); i <= maxLine; ++i)
- {
- auto& line = *lines.getUnchecked(i);
- auto len = line.lineLength;
-
- if (i == startLine)
- {
- auto index = start.getIndexInLine();
- mo << line.line.substring (index, len);
- }
- else if (i == endLine)
- {
- len = end.getIndexInLine();
- mo << line.line.substring (0, len);
- }
- else
- {
- mo << line.line;
- }
- }
-
- return mo.toUTF8();
- }
-
- int CodeDocument::getNumCharacters() const noexcept
- {
- if (auto* lastLine = lines.getLast())
- return lastLine->lineStartInFile + lastLine->lineLength;
-
- return 0;
- }
-
- String CodeDocument::getLine (const int lineIndex) const noexcept
- {
- if (auto* line = lines[lineIndex])
- return line->line;
-
- return {};
- }
-
- int CodeDocument::getMaximumLineLength() noexcept
- {
- if (maximumLineLength < 0)
- {
- maximumLineLength = 0;
-
- for (auto* l : lines)
- maximumLineLength = jmax (maximumLineLength, l->lineLength);
- }
-
- return maximumLineLength;
- }
-
- void CodeDocument::deleteSection (const Position& startPosition, const Position& endPosition)
- {
- deleteSection (startPosition.getPosition(), endPosition.getPosition());
- }
-
- void CodeDocument::deleteSection (const int start, const int end)
- {
- remove (start, end, true);
- }
-
- void CodeDocument::insertText (const Position& position, const String& text)
- {
- insertText (position.getPosition(), text);
- }
-
- void CodeDocument::insertText (const int insertIndex, const String& text)
- {
- insert (text, insertIndex, true);
- }
-
- void CodeDocument::replaceSection (const int start, const int end, const String& newText)
- {
- insertText (end, newText);
- deleteSection (start, end);
- }
-
- void CodeDocument::applyChanges (const String& newContent)
- {
- const String corrected (StringArray::fromLines (newContent)
- .joinIntoString (newLineChars));
-
- TextDiff diff (getAllContent(), corrected);
-
- for (auto& c : diff.changes)
- {
- if (c.isDeletion())
- remove (c.start, c.start + c.length, true);
- else
- insert (c.insertedText, c.start, true);
- }
- }
-
- void CodeDocument::replaceAllContent (const String& newContent)
- {
- remove (0, getNumCharacters(), true);
- insert (newContent, 0, true);
- }
-
- bool CodeDocument::loadFromStream (InputStream& stream)
- {
- remove (0, getNumCharacters(), false);
- insert (stream.readEntireStreamAsString(), 0, false);
- setSavePoint();
- clearUndoHistory();
- return true;
- }
-
- bool CodeDocument::writeToStream (OutputStream& stream)
- {
- for (auto* l : lines)
- {
- auto temp = l->line; // use a copy to avoid bloating the memory footprint of the stored string.
- const char* utf8 = temp.toUTF8();
-
- if (! stream.write (utf8, strlen (utf8)))
- return false;
- }
-
- return true;
- }
-
- void CodeDocument::setNewLineCharacters (const String& newChars) noexcept
- {
- jassert (newChars == "\r\n" || newChars == "\n" || newChars == "\r");
- newLineChars = newChars;
- }
-
- void CodeDocument::newTransaction()
- {
- undoManager.beginNewTransaction (String());
- }
-
- void CodeDocument::undo()
- {
- newTransaction();
- undoManager.undo();
- }
-
- void CodeDocument::redo()
- {
- undoManager.redo();
- }
-
- void CodeDocument::clearUndoHistory()
- {
- undoManager.clearUndoHistory();
- }
-
- void CodeDocument::setSavePoint() noexcept
- {
- indexOfSavedState = currentActionIndex;
- }
-
- bool CodeDocument::hasChangedSinceSavePoint() const noexcept
- {
- return currentActionIndex != indexOfSavedState;
- }
-
- //==============================================================================
- static int getCharacterType (juce_wchar character) noexcept
- {
- return (CharacterFunctions::isLetterOrDigit (character) || character == '_')
- ? 2 : (CharacterFunctions::isWhitespace (character) ? 0 : 1);
- }
-
- CodeDocument::Position CodeDocument::findWordBreakAfter (const Position& position) const noexcept
- {
- auto p = position;
- const int maxDistance = 256;
- int i = 0;
-
- while (i < maxDistance
- && CharacterFunctions::isWhitespace (p.getCharacter())
- && (i == 0 || (p.getCharacter() != '\n'
- && p.getCharacter() != '\r')))
- {
- ++i;
- p.moveBy (1);
- }
-
- if (i == 0)
- {
- auto type = getCharacterType (p.getCharacter());
-
- while (i < maxDistance && type == getCharacterType (p.getCharacter()))
- {
- ++i;
- p.moveBy (1);
- }
-
- while (i < maxDistance
- && CharacterFunctions::isWhitespace (p.getCharacter())
- && (i == 0 || (p.getCharacter() != '\n'
- && p.getCharacter() != '\r')))
- {
- ++i;
- p.moveBy (1);
- }
- }
-
- return p;
- }
-
- CodeDocument::Position CodeDocument::findWordBreakBefore (const Position& position) const noexcept
- {
- auto p = position;
- const int maxDistance = 256;
- int i = 0;
- bool stoppedAtLineStart = false;
-
- while (i < maxDistance)
- {
- auto c = p.movedBy (-1).getCharacter();
-
- if (c == '\r' || c == '\n')
- {
- stoppedAtLineStart = true;
-
- if (i > 0)
- break;
- }
-
- if (! CharacterFunctions::isWhitespace (c))
- break;
-
- p.moveBy (-1);
- ++i;
- }
-
- if (i < maxDistance && ! stoppedAtLineStart)
- {
- auto type = getCharacterType (p.movedBy (-1).getCharacter());
-
- while (i < maxDistance && type == getCharacterType (p.movedBy (-1).getCharacter()))
- {
- p.moveBy (-1);
- ++i;
- }
- }
-
- return p;
- }
-
- void CodeDocument::findTokenContaining (const Position& pos, Position& start, Position& end) const noexcept
- {
- auto isTokenCharacter = [] (juce_wchar c) { return CharacterFunctions::isLetterOrDigit (c) || c == '.' || c == '_'; };
-
- end = pos;
- while (isTokenCharacter (end.getCharacter()))
- end.moveBy (1);
-
- start = end;
- while (start.getIndexInLine() > 0
- && isTokenCharacter (start.movedBy (-1).getCharacter()))
- start.moveBy (-1);
- }
-
- void CodeDocument::findLineContaining (const Position& pos, Position& s, Position& e) const noexcept
- {
- s.setLineAndIndex (pos.getLineNumber(), 0);
- e.setLineAndIndex (pos.getLineNumber() + 1, 0);
- }
-
- void CodeDocument::checkLastLineStatus()
- {
- while (lines.size() > 0
- && lines.getLast()->lineLength == 0
- && (lines.size() == 1 || ! lines.getUnchecked (lines.size() - 2)->endsWithLineBreak()))
- {
- // remove any empty lines at the end if the preceding line doesn't end in a newline.
- lines.removeLast();
- }
-
- const CodeDocumentLine* const lastLine = lines.getLast();
-
- if (lastLine != nullptr && lastLine->endsWithLineBreak())
- {
- // check that there's an empty line at the end if the preceding one ends in a newline..
- lines.add (new CodeDocumentLine (StringRef(), StringRef(), 0, 0,
- lastLine->lineStartInFile + lastLine->lineLength));
- }
- }
-
- //==============================================================================
- void CodeDocument::addListener (CodeDocument::Listener* l) { listeners.add (l); }
- void CodeDocument::removeListener (CodeDocument::Listener* l) { listeners.remove (l); }
-
- //==============================================================================
- struct CodeDocument::InsertAction : public UndoableAction
- {
- InsertAction (CodeDocument& doc, const String& t, const int pos) noexcept
- : owner (doc), text (t), insertPos (pos)
- {
- }
-
- bool perform() override
- {
- owner.currentActionIndex++;
- owner.insert (text, insertPos, false);
- return true;
- }
-
- bool undo() override
- {
- owner.currentActionIndex--;
- owner.remove (insertPos, insertPos + text.length(), false);
- return true;
- }
-
- int getSizeInUnits() override { return text.length() + 32; }
-
- CodeDocument& owner;
- const String text;
- const int insertPos;
-
- JUCE_DECLARE_NON_COPYABLE (InsertAction)
- };
-
- void CodeDocument::insert (const String& text, const int insertPos, const bool undoable)
- {
- if (text.isNotEmpty())
- {
- if (undoable)
- {
- undoManager.perform (new InsertAction (*this, text, insertPos));
- }
- else
- {
- Position pos (*this, insertPos);
- auto firstAffectedLine = pos.getLineNumber();
-
- auto* firstLine = lines[firstAffectedLine];
- auto textInsideOriginalLine = text;
-
- if (firstLine != nullptr)
- {
- auto index = pos.getIndexInLine();
- textInsideOriginalLine = firstLine->line.substring (0, index)
- + textInsideOriginalLine
- + firstLine->line.substring (index);
- }
-
- maximumLineLength = -1;
- Array<CodeDocumentLine*> newLines;
- CodeDocumentLine::createLines (newLines, textInsideOriginalLine);
- jassert (newLines.size() > 0);
-
- auto* newFirstLine = newLines.getUnchecked (0);
- newFirstLine->lineStartInFile = firstLine != nullptr ? firstLine->lineStartInFile : 0;
- lines.set (firstAffectedLine, newFirstLine);
-
- if (newLines.size() > 1)
- lines.insertArray (firstAffectedLine + 1, newLines.getRawDataPointer() + 1, newLines.size() - 1);
-
- int lineStart = newFirstLine->lineStartInFile;
-
- for (int i = firstAffectedLine; i < lines.size(); ++i)
- {
- auto& l = *lines.getUnchecked (i);
- l.lineStartInFile = lineStart;
- lineStart += l.lineLength;
- }
-
- checkLastLineStatus();
- auto newTextLength = text.length();
-
- for (auto* p : positionsToMaintain)
- if (p->getPosition() >= insertPos)
- p->setPosition (p->getPosition() + newTextLength);
-
- listeners.call ([&] (Listener& l) { l.codeDocumentTextInserted (text, insertPos); });
- }
- }
- }
-
- //==============================================================================
- struct CodeDocument::DeleteAction : public UndoableAction
- {
- DeleteAction (CodeDocument& doc, int start, int end) noexcept
- : owner (doc), startPos (start), endPos (end),
- removedText (doc.getTextBetween (CodeDocument::Position (doc, start),
- CodeDocument::Position (doc, end)))
- {
- }
-
- bool perform() override
- {
- owner.currentActionIndex++;
- owner.remove (startPos, endPos, false);
- return true;
- }
-
- bool undo() override
- {
- owner.currentActionIndex--;
- owner.insert (removedText, startPos, false);
- return true;
- }
-
- int getSizeInUnits() override { return (endPos - startPos) + 32; }
-
- CodeDocument& owner;
- const int startPos, endPos;
- const String removedText;
-
- JUCE_DECLARE_NON_COPYABLE (DeleteAction)
- };
-
- void CodeDocument::remove (const int startPos, const int endPos, const bool undoable)
- {
- if (endPos <= startPos)
- return;
-
- if (undoable)
- {
- undoManager.perform (new DeleteAction (*this, startPos, endPos));
- }
- else
- {
- Position startPosition (*this, startPos);
- Position endPosition (*this, endPos);
-
- maximumLineLength = -1;
- auto firstAffectedLine = startPosition.getLineNumber();
- auto endLine = endPosition.getLineNumber();
- auto& firstLine = *lines.getUnchecked (firstAffectedLine);
-
- if (firstAffectedLine == endLine)
- {
- firstLine.line = firstLine.line.substring (0, startPosition.getIndexInLine())
- + firstLine.line.substring (endPosition.getIndexInLine());
- firstLine.updateLength();
- }
- else
- {
- auto& lastLine = *lines.getUnchecked (endLine);
-
- firstLine.line = firstLine.line.substring (0, startPosition.getIndexInLine())
- + lastLine.line.substring (endPosition.getIndexInLine());
- firstLine.updateLength();
-
- int numLinesToRemove = endLine - firstAffectedLine;
- lines.removeRange (firstAffectedLine + 1, numLinesToRemove);
- }
-
- for (int i = firstAffectedLine + 1; i < lines.size(); ++i)
- {
- auto& l = *lines.getUnchecked (i);
- auto& previousLine = *lines.getUnchecked (i - 1);
- l.lineStartInFile = previousLine.lineStartInFile + previousLine.lineLength;
- }
-
- checkLastLineStatus();
- auto totalChars = getNumCharacters();
-
- for (auto* p : positionsToMaintain)
- {
- if (p->getPosition() > startPosition.getPosition())
- p->setPosition (jmax (startPos, p->getPosition() + startPos - endPos));
-
- if (p->getPosition() > totalChars)
- p->setPosition (totalChars);
- }
-
- listeners.call ([=] (Listener& l) { l.codeDocumentTextDeleted (startPos, endPos); });
- }
- }
-
- //==============================================================================
- //==============================================================================
- #if JUCE_UNIT_TESTS
-
- struct CodeDocumentTest : public UnitTest
- {
- CodeDocumentTest()
- : UnitTest ("CodeDocument", UnitTestCategories::text)
- {}
-
- void runTest() override
- {
- const juce::String jabberwocky ("'Twas brillig, and the slithy toves\n"
- "Did gyre and gimble in the wabe;\n"
- "All mimsy were the borogoves,\n"
- "And the mome raths outgrabe.\n\n"
-
- "'Beware the Jabberwock, my son!\n"
- "The jaws that bite, the claws that catch!\n"
- "Beware the Jubjub bird, and shun\n"
- "The frumious Bandersnatch!'");
-
- {
- beginTest ("Basic checks");
- CodeDocument d;
- d.replaceAllContent (jabberwocky);
-
- expectEquals (d.getNumLines(), 9);
- expect (d.getLine (0).startsWith ("'Twas brillig"));
- expect (d.getLine (2).startsWith ("All mimsy"));
- expectEquals (d.getLine (4), String ("\n"));
- }
-
- {
- beginTest ("Insert/replace/delete");
-
- CodeDocument d;
- d.replaceAllContent (jabberwocky);
-
- d.insertText (CodeDocument::Position (d, 0, 6), "very ");
- expect (d.getLine (0).startsWith ("'Twas very brillig"),
- "Insert text within a line");
-
- d.replaceSection (74, 83, "Quite hungry");
- expectEquals (d.getLine (2), String ("Quite hungry were the borogoves,\n"),
- "Replace section at start of line");
-
- d.replaceSection (11, 18, "cold");
- expectEquals (d.getLine (0), String ("'Twas very cold, and the slithy toves\n"),
- "Replace section within a line");
-
- d.deleteSection (CodeDocument::Position (d, 2, 0), CodeDocument::Position (d, 2, 6));
- expectEquals (d.getLine (2), String ("hungry were the borogoves,\n"),
- "Delete section within a line");
-
- d.deleteSection (CodeDocument::Position (d, 2, 6), CodeDocument::Position (d, 5, 11));
- expectEquals (d.getLine (2), String ("hungry Jabberwock, my son!\n"),
- "Delete section across multiple line");
- }
-
- {
- beginTest ("Line splitting and joining");
-
- CodeDocument d;
- d.replaceAllContent (jabberwocky);
- expectEquals (d.getNumLines(), 9);
-
- const String splitComment ("Adding a newline should split a line into two.");
- d.insertText (49, "\n");
-
- expectEquals (d.getNumLines(), 10, splitComment);
- expectEquals (d.getLine (1), String ("Did gyre and \n"), splitComment);
- expectEquals (d.getLine (2), String ("gimble in the wabe;\n"), splitComment);
-
- const String joinComment ("Removing a newline should join two lines.");
- d.deleteSection (CodeDocument::Position (d, 0, 35),
- CodeDocument::Position (d, 1, 0));
-
- expectEquals (d.getNumLines(), 9, joinComment);
- expectEquals (d.getLine (0), String ("'Twas brillig, and the slithy tovesDid gyre and \n"), joinComment);
- expectEquals (d.getLine (1), String ("gimble in the wabe;\n"), joinComment);
- }
-
- {
- beginTest ("Undo/redo");
-
- CodeDocument d;
- d.replaceAllContent (jabberwocky);
- d.newTransaction();
- d.insertText (30, "INSERT1");
- d.newTransaction();
- d.insertText (70, "INSERT2");
- d.undo();
-
- expect (d.getAllContent().contains ("INSERT1"), "1st edit should remain.");
- expect (! d.getAllContent().contains ("INSERT2"), "2nd edit should be undone.");
-
- d.redo();
- expect (d.getAllContent().contains ("INSERT2"), "2nd edit should be redone.");
-
- d.newTransaction();
- d.deleteSection (25, 90);
- expect (! d.getAllContent().contains ("INSERT1"), "1st edit should be deleted.");
- expect (! d.getAllContent().contains ("INSERT2"), "2nd edit should be deleted.");
- d.undo();
- expect (d.getAllContent().contains ("INSERT1"), "1st edit should be restored.");
- expect (d.getAllContent().contains ("INSERT2"), "1st edit should be restored.");
-
- d.undo();
- d.undo();
- expectEquals (d.getAllContent(), jabberwocky, "Original document should be restored.");
- }
-
- {
- beginTest ("Positions");
-
- CodeDocument d;
- d.replaceAllContent (jabberwocky);
-
- {
- const String comment ("Keeps negative positions inside document.");
- CodeDocument::Position p1 (d, 0, -3);
- CodeDocument::Position p2 (d, -8);
- expectEquals (p1.getLineNumber(), 0, comment);
- expectEquals (p1.getIndexInLine(), 0, comment);
- expectEquals (p1.getCharacter(), juce_wchar ('\''), comment);
- expectEquals (p2.getLineNumber(), 0, comment);
- expectEquals (p2.getIndexInLine(), 0, comment);
- expectEquals (p2.getCharacter(), juce_wchar ('\''), comment);
- }
-
- {
- const String comment ("Moving by character handles newlines correctly.");
- CodeDocument::Position p1 (d, 0, 35);
- p1.moveBy (1);
- expectEquals (p1.getLineNumber(), 1, comment);
- expectEquals (p1.getIndexInLine(), 0, comment);
- p1.moveBy (75);
- expectEquals (p1.getLineNumber(), 3, comment);
- }
-
- {
- const String comment1 ("setPositionMaintained tracks position.");
- const String comment2 ("setPositionMaintained tracks position following undos.");
-
- CodeDocument::Position p1 (d, 3, 0);
- p1.setPositionMaintained (true);
- expectEquals (p1.getCharacter(), juce_wchar ('A'), comment1);
-
- d.newTransaction();
- d.insertText (p1, "INSERT1");
-
- expectEquals (p1.getCharacter(), juce_wchar ('A'), comment1);
- expectEquals (p1.getLineNumber(), 3, comment1);
- expectEquals (p1.getIndexInLine(), 7, comment1);
- d.undo();
- expectEquals (p1.getIndexInLine(), 0, comment2);
-
- d.newTransaction();
- d.insertText (15, "\n");
-
- expectEquals (p1.getLineNumber(), 4, comment1);
- d.undo();
- expectEquals (p1.getLineNumber(), 3, comment2);
- }
- }
-
- {
- beginTest ("Iterators");
-
- CodeDocument d;
- d.replaceAllContent (jabberwocky);
-
- {
- const String comment1 ("Basic iteration.");
- const String comment2 ("Reverse iteration.");
- const String comment3 ("Reverse iteration stops at doc start.");
- const String comment4 ("Check iteration across line boundaries.");
-
- CodeDocument::Iterator it (d);
- expectEquals (it.peekNextChar(), juce_wchar ('\''), comment1);
- expectEquals (it.nextChar(), juce_wchar ('\''), comment1);
- expectEquals (it.nextChar(), juce_wchar ('T'), comment1);
- expectEquals (it.nextChar(), juce_wchar ('w'), comment1);
- expectEquals (it.peekNextChar(), juce_wchar ('a'), comment2);
- expectEquals (it.previousChar(), juce_wchar ('w'), comment2);
- expectEquals (it.previousChar(), juce_wchar ('T'), comment2);
- expectEquals (it.previousChar(), juce_wchar ('\''), comment2);
- expectEquals (it.previousChar(), juce_wchar (0), comment3);
- expect (it.isSOF(), comment3);
-
- while (it.peekNextChar() != juce_wchar ('D')) // "Did gyre..."
- it.nextChar();
-
- expectEquals (it.nextChar(), juce_wchar ('D'), comment3);
- expectEquals (it.peekNextChar(), juce_wchar ('i'), comment3);
- expectEquals (it.previousChar(), juce_wchar ('D'), comment3);
- expectEquals (it.previousChar(), juce_wchar ('\n'), comment3);
- expectEquals (it.previousChar(), juce_wchar ('s'), comment3);
- }
-
- {
- const String comment1 ("Iterator created from CodeDocument::Position objects.");
- const String comment2 ("CodeDocument::Position created from Iterator objects.");
- const String comment3 ("CodeDocument::Position created from EOF Iterator objects.");
-
- CodeDocument::Position p (d, 6, 0); // "The jaws..."
- CodeDocument::Iterator it (p);
-
- expectEquals (it.nextChar(), juce_wchar ('T'), comment1);
- expectEquals (it.nextChar(), juce_wchar ('h'), comment1);
- expectEquals (it.previousChar(), juce_wchar ('h'), comment1);
- expectEquals (it.previousChar(), juce_wchar ('T'), comment1);
- expectEquals (it.previousChar(), juce_wchar ('\n'), comment1);
- expectEquals (it.previousChar(), juce_wchar ('!'), comment1);
-
- const auto p2 = it.toPosition();
- expectEquals (p2.getLineNumber(), 5, comment2);
- expectEquals (p2.getIndexInLine(), 30, comment2);
-
- while (! it.isEOF())
- it.nextChar();
-
- const auto p3 = it.toPosition();
- expectEquals (p3.getLineNumber(), d.getNumLines() - 1, comment3);
- expectEquals (p3.getIndexInLine(), d.getLine (d.getNumLines() - 1).length(), comment3);
- }
- }
- }
- };
-
- static CodeDocumentTest codeDocumentTests;
-
- #endif
-
- } // namespace juce
|