TextEditor: piece-tree editor on the current AVL tree #11

Merged
ilusha merged 1 commit from TextEditor into master 2026-10-29 13:01:38 +03:00
5 changed files with 989 additions and 0 deletions

View file

@ -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)

13
TextEditor/CMakeLists.txt Normal file
View file

@ -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})

View file

@ -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<Index>& 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<Index> 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<PiecesDS::Node*> 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; }

View file

@ -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<Index>& 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 <typename NodeType>
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<PieceKey, Piece> 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<Char> mBuff;
Buffer<Index> mLineOffsets;
AddBuff();
void append(const Input& aInput);
Pointer endPtr() const;
};
struct OrigBuff {
bool mOwnsInput = false;
Input mOriginal;
Buffer<Index> 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;
};
}

View file

@ -0,0 +1,261 @@
#include "UnitTest++/UnitTest++.h"
#include "TextEditor.hpp"
#include <cstdlib>
#include <cstring>
#include <string>
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(); }