From e045603df8926fd8a01d41eac15a6f43ab05f2da Mon Sep 17 00:00:00 2001 From: agent Date: Thu, 29 Oct 2026 13:06:36 +0300 Subject: [PATCH] TextEditor: piece-tree editor on the current AVL tree Restores the piece-table text editor from agent-work on the merged Containers AVL tree, with the test build no longer gated behind the undefined MODULES_BUILD_TESTS option. The module is registered in the root build. --- CMakeLists.txt | 1 + TextEditor/CMakeLists.txt | 13 + TextEditor/private/TextEditor.cpp | 506 +++++++++++++++++++++++++++ TextEditor/public/TextEditor.hpp | 208 +++++++++++ TextEditor/tests/TextEditorTests.cpp | 261 ++++++++++++++ 5 files changed, 989 insertions(+) create mode 100644 TextEditor/CMakeLists.txt create mode 100644 TextEditor/private/TextEditor.cpp create mode 100644 TextEditor/public/TextEditor.hpp create mode 100644 TextEditor/tests/TextEditorTests.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index be7af340..ef9a5db6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,6 +25,7 @@ add_subdirectory(Math) add_subdirectory(Callstack) add_subdirectory(Allocators) add_subdirectory(Strings) +add_subdirectory(TextEditor) add_subdirectory(Language) add_subdirectory(Connection) add_subdirectory(GraphicApplication) diff --git a/TextEditor/CMakeLists.txt b/TextEditor/CMakeLists.txt new file mode 100644 index 00000000..8dec5d4e --- /dev/null +++ b/TextEditor/CMakeLists.txt @@ -0,0 +1,13 @@ +project(TextEditor) + +file(GLOB SOURCES "./private/*.cpp") +file(GLOB HEADERS "./public/*.hpp") +add_library(${PROJECT_NAME} STATIC ${SOURCES} ${HEADERS}) +target_include_directories(${PROJECT_NAME} PUBLIC ./public/) +target_link_libraries(${PROJECT_NAME} PUBLIC Containers) + +### -------------------------- Tests -------------------------- ### +file(GLOB TEST_SOURCES "./tests/*.cpp") +add_executable(Test${PROJECT_NAME} ${TEST_SOURCES}) +target_link_libraries(Test${PROJECT_NAME} ${PROJECT_NAME} UnitTest++) +add_test(NAME Test${PROJECT_NAME} COMMAND Test${PROJECT_NAME}) diff --git a/TextEditor/private/TextEditor.cpp b/TextEditor/private/TextEditor.cpp new file mode 100644 index 00000000..37b7f305 --- /dev/null +++ b/TextEditor/private/TextEditor.cpp @@ -0,0 +1,506 @@ +#include "TextEditor.hpp" + +#include "Utils.hpp" + +using namespace tp; + +bool tp::isNewLineChar(Char character) { return (character == '\n') || (character == '\r'); } + +void Input::getLineOffsets(Buffer& out) const { + out.append(0); + + for (Index idx = 0; idx < mLen; idx++) { + if (isNewLineChar(mInput[idx])) { + out.append(idx + 1); + } + } + + out.append(mLen); +} + +// ---------------------- Pointer ---------------------- // + +Pointer::Pointer() : + mLine(0), + mCol(0) {} + +Pointer::Pointer(LineId aLine, ColumnId aCol) : + mLine(aLine), + mCol(aCol) {} + +Pointer::Pointer(const Input& aInput) : + Pointer() { + for (Index idx = 0; idx < aInput.mLen; idx++) { + *this += aInput.mInput[idx]; + } +} + +void Pointer::operator+=(Char achar) { + if (isNewLineChar(achar)) { + mLine += 1; + mCol = 0; + return; + } + + mCol += 1; +} + +void Pointer::operator+=(const Pointer& ap) { *this = *this + ap; } + +Pointer Pointer::operator-(const Pointer& in) const { + if (mLine == in.mLine) { + return { 0, mCol - in.mCol }; + } + return { mLine - in.mLine, mCol }; +} + +Pointer Pointer::operator+(const Pointer& ap) const { + if (ap.mLine == 0) { + return { mLine, mCol + ap.mCol }; + } + return { mLine + ap.mLine, ap.mCol }; +} + +bool Pointer::operator==(const Pointer& in) const { return in.mCol == mCol && in.mLine == mLine; } + +bool Pointer::operator!=(const Pointer& in) const { return !(*this == in); } + +bool Pointer::operator>(const Pointer& in) const { + if (mLine != in.mLine) { + return mLine > in.mLine; + } + return mCol > in.mCol; +} + +bool Pointer::operator>=(const Pointer& in) const { return *this > in || *this == in; } + +Pointer Piece::diffPtr() const { return mEnd - mStart; } + +LineId Piece::nLines() const { return mEnd.mLine - mStart.mLine; } + +// ---------------------- Piece Key ---------------------- // + +PieceKey PieceKey::forLookup(Pointer position) { + PieceKey out; + out.mLookupPosition = position; + return out; +} + +bool PieceKey::descentRight(const PieceKey& query) const { return query.mLookupPosition >= mLeftSpan + mSpan; } + +bool PieceKey::exactNode(const PieceKey& query) const { + return query.mLookupPosition >= mLeftSpan && mLeftSpan + mSpan > query.mLookupPosition; +} + +PieceKey PieceKey::keyInRightSubtree(const PieceKey& query) const { + return forLookup(query.mLookupPosition - (mLeftSpan + mSpan)); +} + +PieceKey PieceKey::keyInLeftSubtree(const PieceKey& query) const { return query; } + +Pointer PieceKey::span() const { return mSpan; } + +Pointer PieceKey::leftSpan() const { return mLeftSpan; } + +Pointer PieceKey::subtreeSpan() const { return (mLeftSpan + mSpan) + mRightSpan; } + +// ---------------------- Pieces ---------------------- // + +auto PiecesDS::find(Pointer position) const -> Node* { return mTree.find(PieceKey::forLookup(position)); } + +auto PiecesDS::first() const -> Node* { return mTree.minNode(mTree.head()); } + +auto PiecesDS::last() const -> Node* { return mTree.maxNode(mTree.head()); } + +auto PiecesDS::next(Node* node) const -> Node* { return mTree.next(node); } + +auto PiecesDS::prev(Node* node) const -> Node* { return mTree.prev(node); } + +Pointer PiecesDS::position(const Node* node) const { + Pointer out = node->key.leftSpan(); + + for (const Node* iter = node; iter->mParent; iter = iter->mParent) { + const Node* parent = iter->mParent; + if (parent->mRight == iter) { + out = (parent->key.leftSpan() + parent->key.span()) + out; + } + } + + return out; +} + +Pointer PiecesDS::ptrSum() const { + if (!mTree.head()) { + return { 0, 0 }; + } + return mTree.head()->key.subtreeSpan(); +} + +ualni PiecesDS::nPieces() const { return mTree.size(); } + +auto PiecesDS::insertAfter(Node* predecessor, const Piece& piece) -> Node* { + return mTree.insertAfter(predecessor, PieceKey(), piece); +} + +void PiecesDS::remove(Node* node) { mTree.removeNode(node); } + +void PiecesDS::resize(Node* node, Pointer newEnd) { + node->data.mEnd = newEnd; + mTree.refreshNode(node); +} + +// ---------------------- Add Buff ---------------------- // + +AddBuff::AddBuff() { + Input empty; + empty.getLineOffsets(mLineOffsets); +} + +void AddBuff::append(const Input& aInput) { + Buffer inputOffsets; + aInput.getLineOffsets(inputOffsets); + + const auto appendStart = Index(mBuff.size()); + + mLineOffsets.pop(); + for (ualni idx = 1; idx < inputOffsets.size(); idx++) { + mLineOffsets.append(appendStart + inputOffsets[idx]); + } + + for (Index idx = 0; idx < aInput.mLen; idx++) { + mBuff.append(aInput.mInput[idx]); + } +} + +Pointer AddBuff::endPtr() const { + const auto lastLine = mLineOffsets.size() - 2; + return { LineId(lastLine), mLineOffsets.last() - mLineOffsets[lastLine] }; +} + +// ---------------------- Orig Buff ---------------------- // + +OrigBuff::OrigBuff(Input aOriginal) : + mOriginal(aOriginal) { + mOriginal.getLineOffsets(mLineOffsets); + mPtrStart = { 0, 0 }; + mPtrEnd = Pointer(mOriginal); +} + +void OrigBuff::makeOwnOriginalInput() { + if (mOwnsInput) { + return; + } + + auto inputCopy = new Char[mOriginal.mLen + 1]; + memCopy(inputCopy, mOriginal.mInput, mOriginal.mLen); + inputCopy[mOriginal.mLen] = '\0'; + + mOriginal.mInput = inputCopy; + mOwnsInput = true; +} + +OrigBuff::~OrigBuff() { + if (mOwnsInput) { + delete[] mOriginal.mInput; + } +} + +// ---------------------- Editor ---------------------- // + +TextEditor::TextEditor() : + mOrigBuff(Input()) {} + +TextEditor::TextEditor(Input original) : + mOrigBuff(original) { + if (original.mLen) { + mPieces.insertAfter(nullptr, { false, mOrigBuff.mPtrStart, mOrigBuff.mPtrEnd }); + } + mPieces.pushUndoPoint(); +} + +PieceCrs TextEditor::findPiece(Pointer ptr) const { + PieceCrs out; + if (!mPieces.nPieces()) { + return out; + } + + out.piece = mPieces.ptrSum() == ptr ? mPieces.last() : mPieces.find(ptr); + if (!out.piece) { + return out; + } + + out.piece_ptr = mPieces.position(out.piece); + out.ptr = ptr; + out.offset = out.ptr - out.piece_ptr; + return out; +} + +Index TextEditor::mNLines() const { return mPieces.ptrSum().mLine; } + +Pointer TextEditor::getCursor() const { return mCursor; } + +Pointer TextEditor::getCursorEnd() const { return mCursorEnd; } + +void TextEditor::clampPtr(Pointer& ptr) const { + if (!mPieces.nPieces()) { + ptr = { 0, 0 }; + return; + } + + ptr.mLine = clamp(ptr.mLine, 0, mNLines()); + + auto crs = findPiece(Pointer{ ptr.mLine, 0 }); + Index lineLength = getPieceLineLen(crs.piece->data, crs.offset.mLine); + + auto piece = crs.piece; + auto nextStart = nextPiecePtr(piece); + while (mPieces.next(piece) && ptr.mLine == nextStart.mLine) { + piece = mPieces.next(piece); + nextStart = nextPiecePtr(piece); + lineLength += getPieceLineLen(piece->data, 0); + } + + if (ptr.mLine != mNLines()) { + lineLength -= 1; + } + + ptr.mCol = clamp(ptr.mCol, 0, lineLength); +} + +void TextEditor::setCursor(Pointer ptr) { + clampPtr(ptr); + mCursor = ptr; +} + +void TextEditor::setCursorEnd(Pointer ptr) { + clampPtr(ptr); + mCursorEnd = ptr; + mCursor = mCursorEnd; +} + +const Char* TextEditor::getBuffLine(const Piece& piece, LineId line) const { + if (piece.mAddedBuff) { + return mAddBuff.mBuff.getBuff() + mAddBuff.mLineOffsets[line]; + } + return mOrigBuff.mOriginal.mInput + mOrigBuff.mLineOffsets[line]; +} + +const Char* TextEditor::getPieceLine(const Piece& piece, LineId line) const { + if (line == 0) { + return getBuffLine(piece, piece.mStart.mLine) + piece.mStart.mCol; + } + if (line > piece.nLines()) { + return getBuffLine(piece, piece.mEnd.mLine) + piece.mEnd.mCol; + } + return getBuffLine(piece, piece.mStart.mLine + line); +} + +LineId TextEditor::getPieceLineLen(const Piece& piece, LineId line) const { + return LineId(getPieceLine(piece, line + 1) - getPieceLine(piece, line)); +} + +Pointer TextEditor::prevPiecePtr(PiecesDS::Node* pieceNode) const { + auto previous = mPieces.prev(pieceNode); + if (!previous) { + return { 0, 0 }; + } + return mPieces.position(previous); +} + +Pointer TextEditor::nextPiecePtr(PiecesDS::Node* pieceNode) const { + return mPieces.position(pieceNode) + pieceNode->data.diffPtr(); +} + +void TextEditor::insert(Input aInput) { + remove(true); + + auto startRef = mAddBuff.endPtr(); + mAddBuff.append(aInput); + auto endRef = mAddBuff.endPtr(); + auto inputDiff = endRef - startRef; + + auto crs = findPiece(mCursor); + + if (crs.piece && mPieces.prev(crs.piece) && crs.offset == Pointer{ 0, 0 }) { + crs.piece = mPieces.prev(crs.piece); + crs.piece_ptr = mPieces.position(crs.piece); + crs.offset = crs.ptr - crs.piece_ptr; + } + + PiecesDS::Node* predecessor = nullptr; + bool extended = false; + + if (crs.piece) { + const Piece& piece = crs.piece->data; + + if (crs.offset == Pointer{ 0, 0 }) { + predecessor = mPieces.prev(crs.piece); + } else if (crs.offset == piece.diffPtr()) { + const bool continuesLastInsertion = mLastInsertionCrs == crs.ptr && piece.mAddedBuff && piece.mEnd == startRef; + if (continuesLastInsertion) { + mPieces.resize(crs.piece, piece.mEnd + inputDiff); + extended = true; + } else { + predecessor = crs.piece; + } + } else { + predecessor = splitPiece(crs.piece, crs.offset); + } + } + + if (!extended) { + mPieces.insertAfter(predecessor, { true, startRef, endRef }); + } + + mCursor = mCursor + inputDiff; + mCursorEnd = mCursor; + mLastInsertionCrs = mCursor; + mPieces.pushUndoPoint(); +} + +PiecesDS::Node* TextEditor::splitPiece(PiecesDS::Node* piece, Pointer splitPtr) { + auto span = piece->data.diffPtr(); + + if (span == splitPtr || splitPtr == Pointer{ 0, 0 }) { + return nullptr; + } + + const Piece original = piece->data; + const Pointer splitAt = original.mStart + splitPtr; + + mPieces.resize(piece, splitAt); + mPieces.insertAfter(piece, { original.mAddedBuff, splitAt, original.mEnd }); + + return piece; +} + +void TextEditor::removeRange(PiecesDS::Node* first, PiecesDS::Node* last) { + Buffer doomed; + + for (auto iter = first; iter; iter = mPieces.next(iter)) { + doomed.append(iter); + if (iter == last) { + break; + } + } + + for (auto node : doomed) { + mPieces.remove(node.data()); + } +} + +void TextEditor::remove(bool only_selection, Dir dir, DirUnit unit) { + if (!mPieces.nPieces()) { + return; + } + + if (mCursor == mCursorEnd) { + if (only_selection) { + return; + } + + Pointer neighbour = mCursor; + + if (dir == LEFT) { + if (mCursor == Pointer(0, 0)) { + return; + } + + if (mCursor.mCol == 0) { + neighbour = Pointer{ mCursor.mLine - 1, ENV_HALNI_MAX }; + clampPtr(neighbour); + } else { + neighbour = Pointer{ mCursor.mLine, mCursor.mCol - 1 }; + } + } else if (dir == RIGHT) { + Pointer lineEnd{ mCursor.mLine, ENV_HALNI_MAX }; + clampPtr(lineEnd); + + if (mCursor.mCol < lineEnd.mCol) { + neighbour = Pointer{ mCursor.mLine, mCursor.mCol + 1 }; + } else if (mCursor.mLine < mNLines()) { + neighbour = Pointer{ mCursor.mLine + 1, 0 }; + } else { + return; + } + } else { + return; + } + + mCursorEnd = neighbour; + } + + if (mCursor > mCursorEnd) { + swapV(mCursor, mCursorEnd); + } + + auto crs = findPiece(mCursor); + + PiecesDS::Node* firstRemoved = nullptr; + if (crs.offset == Pointer{ 0, 0 }) { + firstRemoved = crs.piece; + } else if (crs.piece->data.diffPtr() == crs.offset) { + firstRemoved = mPieces.next(crs.piece); + } else { + firstRemoved = mPieces.next(splitPiece(crs.piece, crs.offset)); + } + + auto crsEnd = findPiece(mCursorEnd); + + PiecesDS::Node* lastRemoved = nullptr; + if (crsEnd.offset == Pointer{ 0, 0 }) { + lastRemoved = mPieces.prev(crsEnd.piece); + } else if (crsEnd.piece->data.diffPtr() == crsEnd.offset) { + lastRemoved = crsEnd.piece; + } else { + lastRemoved = splitPiece(crsEnd.piece, crsEnd.offset); + } + + DEBUG_ASSERT(firstRemoved && lastRemoved); + removeRange(firstRemoved, lastRemoved); + + mCursorEnd = mCursor; + mPieces.pushUndoPoint(); +} + +void TextEditor::undo() { mPieces.undo(); } + +void TextEditor::redo() { mPieces.redo(); } + +// ------------------------------ ITERATOR ------------------------------ // + +TextEditor::Iterator TextEditor::begin() const { return Iterator(this); } + +TextEditor::IteratorEnd TextEditor::end() const { return {}; } + +TextEditor::Iterator::Iterator(const TextEditor* self) : + mSelf(self), + mNode(self->mPieces.first()) { + loadCharacter(); +} + +void TextEditor::Iterator::loadCharacter() { + if (!mNode) { + return; + } + mVal = mSelf->getPieceLine(mNode->data, mOffset.mLine)[mOffset.mCol]; +} + +Char TextEditor::Iterator::character() const { return mVal; } + +void TextEditor::Iterator::operator++() { + mOffset += mVal; + + if (mOffset == mNode->data.diffPtr()) { + mNode = mSelf->mPieces.next(mNode); + mOffset = { 0, 0 }; + } + + loadCharacter(); +} + +bool TextEditor::Iterator::operator!=(const IteratorEnd&) const { return mNode != nullptr; } + +bool TextEditor::Iterator::operator==(const IteratorEnd&) const { return mNode == nullptr; } + +const TextEditor::Iterator& TextEditor::Iterator::operator*() const { return *this; } diff --git a/TextEditor/public/TextEditor.hpp b/TextEditor/public/TextEditor.hpp new file mode 100644 index 00000000..ee4ddd28 --- /dev/null +++ b/TextEditor/public/TextEditor.hpp @@ -0,0 +1,208 @@ +#pragma once + +#include "Buffer.hpp" +#include "Tree.hpp" + +namespace tp { + + typedef char Char; + typedef halni Index; + typedef halni LineId; + typedef halni ColumnId; + + bool isNewLineChar(Char character); + + struct Input { + const Char* mInput = nullptr; + Index mLen = 0; + + void getLineOffsets(Buffer& out) const; + }; + + struct Pointer { + LineId mLine; + ColumnId mCol; + + Pointer(); + Pointer(LineId aLine, ColumnId aCol); + explicit Pointer(const Input& aInput); + + void operator+=(Char achar); + void operator+=(const Pointer& ap); + Pointer operator-(const Pointer& in) const; + Pointer operator+(const Pointer& ap) const; + bool operator==(const Pointer& in) const; + bool operator!=(const Pointer& in) const; + bool operator>(const Pointer& in) const; + bool operator>=(const Pointer& in) const; + }; + + struct Piece { + bool mAddedBuff = false; + Pointer mStart; + Pointer mEnd; + + Pointer diffPtr() const; + LineId nLines() const; + }; + + class PieceKey { + public: + PieceKey() = default; + + static PieceKey forLookup(Pointer position); + + bool descentRight(const PieceKey& query) const; + bool exactNode(const PieceKey& query) const; + PieceKey keyInRightSubtree(const PieceKey& query) const; + PieceKey keyInLeftSubtree(const PieceKey& query) const; + + template + void updateNodeCache(const NodeType* node) { + mSpan = node->data.diffPtr(); + mLeftSpan = node->mLeft->key.subtreeSpan(); + mRightSpan = node->mRight->key.subtreeSpan(); + } + + Pointer span() const; + Pointer leftSpan() const; + Pointer subtreeSpan() const; + + private: + Pointer mSpan; + Pointer mLeftSpan; + Pointer mRightSpan; + Pointer mLookupPosition; + }; + + class PiecesDS { + typedef AvlTree Tree; + + public: + typedef Tree::Node Node; + + PiecesDS() = default; + + Node* find(Pointer position) const; + Node* first() const; + Node* last() const; + Node* next(Node* node) const; + Node* prev(Node* node) const; + + Pointer position(const Node* node) const; + Pointer ptrSum() const; + ualni nPieces() const; + + Node* insertAfter(Node* predecessor, const Piece& piece); + void remove(Node* node); + void resize(Node* node, Pointer newEnd); + + void undo() {} + void redo() {} + void pushUndoPoint() {} + + private: + Tree mTree; + }; + + struct AddBuff { + Buffer mBuff; + Buffer mLineOffsets; + + AddBuff(); + void append(const Input& aInput); + Pointer endPtr() const; + }; + + struct OrigBuff { + bool mOwnsInput = false; + Input mOriginal; + Buffer mLineOffsets; + + Pointer mPtrStart; + Pointer mPtrEnd; + + explicit OrigBuff(Input aOriginal); + OrigBuff(const OrigBuff&) = delete; + OrigBuff& operator=(const OrigBuff&) = delete; + ~OrigBuff(); + + void makeOwnOriginalInput(); + }; + + struct PieceCrs { + Pointer piece_ptr; + Pointer offset; + Pointer ptr; + PiecesDS::Node* piece = nullptr; + }; + + class TextEditor { + public: + struct IteratorEnd {}; + + class Iterator { + friend TextEditor; + + public: + Char character() const; + void operator++(); + bool operator!=(const IteratorEnd&) const; + bool operator==(const IteratorEnd&) const; + const Iterator& operator*() const; + + private: + explicit Iterator(const TextEditor* self); + void loadCharacter(); + + const TextEditor* mSelf; + PiecesDS::Node* mNode = nullptr; + Pointer mOffset = { 0, 0 }; + Char mVal = 0; + }; + + enum Dir : uint1 { LEFT, RIGHT, UP, DOWN }; + enum DirUnit : uint1 { CHAR, WORD, TOK, LINE_END, LINE_START }; + + public: + TextEditor(); + explicit TextEditor(Input original); + + [[nodiscard]] Index mNLines() const; + void clampPtr(Pointer& ptr) const; + [[nodiscard]] Pointer getCursor() const; + [[nodiscard]] Pointer getCursorEnd() const; + void setCursor(Pointer ptr); + void setCursorEnd(Pointer ptr); + + void insert(Input aInput); + void remove(bool only_selection = false, Dir dir = LEFT, DirUnit unit = CHAR); + + void undo(); + void redo(); + + Iterator begin() const; + IteratorEnd end() const; + + private: + [[nodiscard]] PieceCrs findPiece(Pointer ptr) const; + [[nodiscard]] Pointer prevPiecePtr(PiecesDS::Node* piece_node) const; + [[nodiscard]] Pointer nextPiecePtr(PiecesDS::Node* piece_node) const; + + [[nodiscard]] const Char* getBuffLine(const Piece& piece, LineId line) const; + [[nodiscard]] const Char* getPieceLine(const Piece& piece, LineId line) const; + [[nodiscard]] LineId getPieceLineLen(const Piece& piece, LineId line) const; + [[nodiscard]] PiecesDS::Node* splitPiece(PiecesDS::Node* piece, Pointer split_ptr); + + void removeRange(PiecesDS::Node* first, PiecesDS::Node* last); + + private: + PiecesDS mPieces; + OrigBuff mOrigBuff; + AddBuff mAddBuff; + + Pointer mCursor; + Pointer mCursorEnd; + Pointer mLastInsertionCrs; + }; +} diff --git a/TextEditor/tests/TextEditorTests.cpp b/TextEditor/tests/TextEditorTests.cpp new file mode 100644 index 00000000..b3b54829 --- /dev/null +++ b/TextEditor/tests/TextEditorTests.cpp @@ -0,0 +1,261 @@ +#include "UnitTest++/UnitTest++.h" + +#include "TextEditor.hpp" + +#include +#include +#include + +using namespace tp; + +namespace { + Input inputOf(const char* text) { return { text, Index(std::strlen(text)) }; } + + std::string contentsOf(const TextEditor& editor) { + std::string out; + for (auto iter = editor.begin(); iter != editor.end(); ++iter) { + out += iter.character(); + } + return out; + } + + Pointer pointerAt(const std::string& text, size_t offset) { + Pointer out; + for (size_t idx = 0; idx < offset; idx++) { + out += text[idx]; + } + return out; + } + + void placeCaret(TextEditor& editor, Pointer at) { editor.setCursorEnd(at); } + + void select(TextEditor& editor, Pointer from, Pointer to) { + editor.setCursorEnd(to); + editor.setCursor(from); + } +} + +SUITE(TextEditor) { + TEST(EmptyEditorHasNoText) { + TextEditor editor; + + CHECK_EQUAL("", contentsOf(editor)); + CHECK_EQUAL(0, editor.mNLines()); + } + + TEST(InsertIntoEmptyEditor) { + TextEditor editor; + + editor.insert(inputOf("abc\nd")); + + CHECK_EQUAL("abc\nd", contentsOf(editor)); + CHECK(editor.getCursor() == Pointer(1, 1)); + } + + TEST(OriginalTextIsReadBack) { + TextEditor editor(inputOf("hello\nworld")); + + CHECK_EQUAL("hello\nworld", contentsOf(editor)); + CHECK_EQUAL(1, editor.mNLines()); + } + + TEST(InsertAtStart) { + TextEditor editor(inputOf("world")); + + placeCaret(editor, { 0, 0 }); + editor.insert(inputOf("hello ")); + + CHECK_EQUAL("hello world", contentsOf(editor)); + } + + TEST(InsertAtEnd) { + TextEditor editor(inputOf("hello")); + + placeCaret(editor, { 0, 5 }); + editor.insert(inputOf("\nworld")); + + CHECK_EQUAL("hello\nworld", contentsOf(editor)); + CHECK(editor.getCursor() == Pointer(1, 5)); + } + + TEST(InsertInTheMiddleSplitsPiece) { + TextEditor editor(inputOf("hello\nworld")); + + placeCaret(editor, { 0, 2 }); + editor.insert(inputOf("XY")); + + CHECK_EQUAL("heXYllo\nworld", contentsOf(editor)); + CHECK(editor.getCursor() == Pointer(0, 4)); + } + + TEST(InsertOnSecondLine) { + TextEditor editor(inputOf("hello\nworld")); + + placeCaret(editor, { 1, 3 }); + editor.insert(inputOf("-")); + + CHECK_EQUAL("hello\nwor-ld", contentsOf(editor)); + } + + TEST(TypingContinuesTheSamePiece) { + TextEditor editor(inputOf("ab")); + + placeCaret(editor, { 0, 1 }); + editor.insert(inputOf("1")); + editor.insert(inputOf("2")); + editor.insert(inputOf("\n3")); + + CHECK_EQUAL("a12\n3b", contentsOf(editor)); + CHECK_EQUAL(1, editor.mNLines()); + } + + TEST(CaretIsClampedToLine) { + TextEditor editor(inputOf("abc\nde")); + + placeCaret(editor, { 0, 100 }); + CHECK(editor.getCursor() == Pointer(0, 3)); + + placeCaret(editor, { 100, 100 }); + CHECK(editor.getCursor() == Pointer(1, 2)); + } + + TEST(BackspaceRemovesPreviousCharacter) { + TextEditor editor(inputOf("hello")); + + placeCaret(editor, { 0, 3 }); + editor.remove(); + + CHECK_EQUAL("helo", contentsOf(editor)); + CHECK(editor.getCursor() == Pointer(0, 2)); + } + + TEST(BackspaceJoinsLines) { + TextEditor editor(inputOf("hello\nworld")); + + placeCaret(editor, { 1, 0 }); + editor.remove(); + + CHECK_EQUAL("helloworld", contentsOf(editor)); + CHECK(editor.getCursor() == Pointer(0, 5)); + } + + TEST(BackspaceAtStartDoesNothing) { + TextEditor editor(inputOf("abc")); + + placeCaret(editor, { 0, 0 }); + editor.remove(); + + CHECK_EQUAL("abc", contentsOf(editor)); + } + + TEST(DeleteRemovesNextCharacter) { + TextEditor editor(inputOf("abc\ndef")); + + placeCaret(editor, { 0, 0 }); + editor.remove(false, TextEditor::RIGHT); + CHECK_EQUAL("bc\ndef", contentsOf(editor)); + + placeCaret(editor, { 0, 2 }); + editor.remove(false, TextEditor::RIGHT); + CHECK_EQUAL("bcdef", contentsOf(editor)); + } + + TEST(DeleteAtEndDoesNothing) { + TextEditor editor(inputOf("abc")); + + placeCaret(editor, { 0, 3 }); + editor.remove(false, TextEditor::RIGHT); + + CHECK_EQUAL("abc", contentsOf(editor)); + } + + TEST(SelectionRemovalAcrossPieces) { + TextEditor editor(inputOf("0123456789")); + + placeCaret(editor, { 0, 5 }); + editor.insert(inputOf("abc")); + CHECK_EQUAL("01234abc56789", contentsOf(editor)); + + select(editor, { 0, 3 }, { 0, 10 }); + editor.remove(true); + + CHECK_EQUAL("012789", contentsOf(editor)); + CHECK(editor.getCursor() == Pointer(0, 3)); + } + + TEST(InsertReplacesSelection) { + TextEditor editor(inputOf("one two three")); + + select(editor, { 0, 4 }, { 0, 7 }); + editor.insert(inputOf("2")); + + CHECK_EQUAL("one 2 three", contentsOf(editor)); + } + + TEST(RemoveEverything) { + TextEditor editor(inputOf("ab\ncd")); + + select(editor, { 0, 0 }, { 1, 2 }); + editor.remove(true); + + CHECK_EQUAL("", contentsOf(editor)); + + editor.insert(inputOf("x")); + CHECK_EQUAL("x", contentsOf(editor)); + } + + TEST(ManyInsertionsKeepOrder) { + TextEditor editor(inputOf(".")); + std::string expected = "."; + + for (int idx = 0; idx < 300; idx++) { + const char letter[] = { char('a' + idx % 26), 0 }; + const size_t offset = (idx * 7) % (expected.size() + 1); + + placeCaret(editor, pointerAt(expected, offset)); + editor.insert(inputOf(letter)); + expected.insert(expected.begin() + offset, letter[0]); + } + + CHECK_EQUAL(expected, contentsOf(editor)); + } + + TEST(RandomEditsMatchReferenceString) { + const char* original = "first line\nsecond line\nthird"; + TextEditor editor(inputOf(original)); + std::string expected = original; + const char* inserts[] = { "x", "yz", "\n", "a\nb", "long insertion" }; + + std::srand(12345); + + for (int step = 0; step < 2000; step++) { + const size_t offset = std::rand() % (expected.size() + 1); + const int action = std::rand() % 3; + + if (action == 0 || expected.empty()) { + const char* text = inserts[std::rand() % 5]; + placeCaret(editor, pointerAt(expected, offset)); + editor.insert(inputOf(text)); + expected.insert(offset, text); + } else if (action == 1 && offset > 0) { + placeCaret(editor, pointerAt(expected, offset)); + editor.remove(); + expected.erase(offset - 1, 1); + } else { + const size_t end = offset + std::rand() % 5; + const size_t clampedEnd = end > expected.size() ? expected.size() : end; + select(editor, pointerAt(expected, offset), pointerAt(expected, clampedEnd)); + editor.remove(true); + expected.erase(offset, clampedEnd - offset); + } + + if (contentsOf(editor) != expected) { + CHECK_EQUAL(expected, contentsOf(editor)); + CHECK_EQUAL(-1, step); + return; + } + } + } +} + +int main() { return UnitTest::RunAllTests(); } -- 2.49.1