TextEditor: piece-tree editor on the current AVL tree #11
5 changed files with 989 additions and 0 deletions
|
|
@ -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
13
TextEditor/CMakeLists.txt
Normal 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})
|
||||
506
TextEditor/private/TextEditor.cpp
Normal file
506
TextEditor/private/TextEditor.cpp
Normal 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; }
|
||||
208
TextEditor/public/TextEditor.hpp
Normal file
208
TextEditor/public/TextEditor.hpp
Normal 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;
|
||||
};
|
||||
}
|
||||
261
TextEditor/tests/TextEditorTests.cpp
Normal file
261
TextEditor/tests/TextEditorTests.cpp
Normal 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(); }
|
||||
Loading…
Add table
Add a link
Reference in a new issue