From 1e7a29328bbfa1acb7b8cee9b2bcf41abc54e24e Mon Sep 17 00:00:00 2001 From: hama Date: Wed, 15 Jul 2015 23:29:03 +0200 Subject: [PATCH] Undo works --- appl/reditor/reditor.pro | 9 +- base/ReFile.cpp | 595 +++++++++++++++++++++++++++++--------- base/ReFile.hpp | 77 ++++- base/ReQStringUtil.cpp | 19 ++ base/ReQStringUtil.hpp | 1 + cunit/cuReFile.cpp | 166 ++++++++++- cunit/cuReQStringUtil.cpp | 10 + gui/ReEdit.cpp | 16 +- gui/ReEdit.hpp | 2 + 9 files changed, 737 insertions(+), 158 deletions(-) diff --git a/appl/reditor/reditor.pro b/appl/reditor/reditor.pro index 8221798..22c86ca 100644 --- a/appl/reditor/reditor.pro +++ b/appl/reditor/reditor.pro @@ -17,13 +17,18 @@ SOURCES += main.cpp\ ../../gui/ReEdit.cpp \ ../../base/ReFile.cpp \ mainwindow.cpp \ - ../../base/ReLogger.cpp + ../../base/ReLogger.cpp \ + ../../base/ReQStringUtil.cpp \ + ../../base/ReException.cpp HEADERS += mainwindow.hpp \ ../../base/rebase.hpp \ ../../gui/regui.hpp \ - ../../gui/ReEdit.hpp + ../../gui/ReEdit.hpp \ + ../../base/ReStringUtil.hpp \ + ../../base/ReQStringUtil.hpp \ + ../../base/ReException.hpp FORMS += mainwindow.ui diff --git a/base/ReFile.cpp b/base/ReFile.cpp index 3d818cc..c616cc8 100644 --- a/base/ReFile.cpp +++ b/base/ReFile.cpp @@ -44,8 +44,8 @@ int memicmp(const void* str1, const void* str2, size_t length) { * Constructor. */ ReLines::ReLines() : - QStringList(), - m_empty() { + QStringList(), + m_empty() { } /** * Destructor. @@ -54,68 +54,136 @@ ReLines::~ReLines() { } /** - * Inserts a text into a given position of the file. + * Checks that the summary size of the undo information remains under the maximum. * - * Note: the line separators will not be stored. + * @param stringSize the additional string size + * @return true: undo info should be stored + * false: too large undo info, all info has + * been freed + */ +bool ReUndoList::checkSummarySize(qint64 stringSize) { + static qint64 sizeStruct = (qint64) sizeof(UndoItem); + while (m_currentUndoSize + stringSize + sizeStruct > m_maxUndoSize) { + if (m_list.length() == 0) + m_currentUndoSize = 0; + else { + UndoItem* item = m_list.at(0); + m_currentUndoSize -= sizeof *item - item->m_string.length(); + m_list.removeAt(0); + delete item; + } + } + return stringSize + sizeStruct < m_maxUndoSize; +} + +/** + * Removes all lines. + */ +void ReLines::clear() { + QStringList::clear(); +} + +/** + * Inserts one or more lines into the lines. * - * @param line the line number (0..N-1) of the insert position - * @param col the column number (0..M-1) of the insert position - * @param text the text to insert. May contain line separators ('\n' or "\r\n"). - * In this case the number of lines grows - */ -void ReLines::insertText(int line, int col, const QString& text) { - if (line >= 0) { - if (line == length()) { - int beginOfLine = 0; - int endOfLine = text.indexOf('\n'); - while (endOfLine >= 0) { - int nextBOL = endOfLine + 1; - if (endOfLine > 0 && text.at(endOfLine - 1) == '\r') - endOfLine--; - append(text.mid(beginOfLine, endOfLine - beginOfLine)); - beginOfLine = nextBOL; - endOfLine = text.indexOf('\n', beginOfLine); + * @param lineNo the line number (0..N-1) of the new line + * @param text the text to insert. May not contain newlines! + * @param withUndo true: prepares undo operation
+ * false: undo is impossible + */ +void ReLines::insertLines(int lineNo, const QString& text, bool withUndo) { + if (lineNo >= 0) { + int count = 0; + if (text.isEmpty()) + count = 1; + else { + count = ReQStringUtil::countOf(text, '\n'); + if (text.at(text.length() - 1) != '\n') + count++; + } + if (withUndo) + storeInsertLines(lineNo, count); + int start = 0; + int end; + if (lineNo >= length()) { + while ((end = text.indexOf('\n', start)) >= 0) { + append(text.mid(start, end - start)); + start = end + 1; } - if (beginOfLine < text.length()) - append(text.mid(beginOfLine)); - } else if (line < length()) { - QString oldLine = at(line); - int length = oldLine.length(); - if (col < 0) - col = 0; - else if (col >= length) - col = length - 1; - QString tail; - if (col < length - 1) - tail = oldLine.mid(col); - int endOfLine = text.indexOf('\n'); - if (endOfLine < 0) { - replace(line, oldLine.mid(0, col) + text + tail); - } else { - int beginOfLine = endOfLine + 1; - if (endOfLine > 0 && text[endOfLine - 1] == '\r') - endOfLine--; - replace(line++, oldLine.mid(0, col) + text.mid(0, endOfLine)); - do { - if ((endOfLine = text.indexOf('\n', beginOfLine)) < 0) - break; - int nextBOL = endOfLine + 1; - if (endOfLine > 0 && text.at(endOfLine - 1) == '\r') - endOfLine--; - insert(line++, - text.mid(beginOfLine, endOfLine - beginOfLine)); - beginOfLine = nextBOL; - } while (true); - if (tail.length() == 0) { - if (beginOfLine < text.length()) - insert(line, text.mid(beginOfLine)); - } else { - if (beginOfLine >= text.length()) - insert(line, tail); - else - insert(line, text.mid(beginOfLine) + tail); - } + if (start < text.length()) + append(text.mid(start)); + } else { + while ((end = text.indexOf('\n', start)) >= 0) { + insert(lineNo++, text.mid(start, end - start)); + start = end + 1; } + if (start < text.length()) + insert(lineNo, text.mid(start)); + } + } +} +/** + * Inserts a text into a given position of the file. + * + * @param lineNo the line number (0..N-1) of the insert position + * @param col the column number (0..M-1) of the insert position
+ * col < 0: no insertion
+ * col > length_of_the_line: the text will be appended to the line + * @param text the text to insert. May not contain newlines! + * @param withUndo true: prepares undo operation
+ * false: undo is impossible + */ +void ReLines::insertPart(int lineNo, int col, const QString& text, + bool withUndo) { + if (lineNo >= 0 && lineNo < lineCount() && col >= 0) { + if (withUndo) + storeInsertPart(lineNo, col, text.length()); + QString current = lineAt(lineNo); + if (col == 0) + replace(lineNo, text + current); + else if (col < current.length()) + replace(lineNo, current.mid(0, col) + text + current.mid(col)); + else + replace(lineNo, current.mid(0, col) + text); + } +} + +/** + * Inserts a text into a given position of the file, with or without newlines. + * + * Note: the line separators will not be stored. + * + * @param lineNo the line number (0..N-1) of the insert position + * @param col the column number (0..M-1) of the insert position + * @param text the text to insert. May contain line separators ('\n' or "\r\n"). + * In this case the number of lines grows + */ +void ReLines::insertText(int lineNo, int col, const QString& text) { + if (length() == 0) + insertLines(0, "", true); + int endOfLine = text.indexOf(QChar('\n')); + if (endOfLine < 0) + insertPart(lineNo, col, text, true); + else { + splitLine(lineNo, col, true); + int newLines = 0; + if (lineNo < length()) + insertPart(lineNo, col, text.mid(0, endOfLine), true); + else + insertLines(lineNo, text.mid(0, endOfLine), true); + int lastEoLn = text.lastIndexOf('\n'); + if (lastEoLn != endOfLine) { + int oldCount = lineCount(); + insertLines(lineNo + 1, + text.mid(endOfLine + 1, lastEoLn - endOfLine), true); + newLines = lineCount() - oldCount; + } + if (lastEoLn != text.length() - 1) { + int nextLine = lineNo + newLines + 1; + if (nextLine < length()) + insertPart(nextLine, 0, text.mid(lastEoLn + 1), true); + else + insertLines(nextLine, text.mid(lastEoLn + 1), true); } } } @@ -140,32 +208,43 @@ bool ReLines::joinLines(int first) { /** * Removes a part of a line. * - * @param line the line number (0..N-1) of the first position to delete - * @param col -1: join the current line with the previous line. - * : join the current line with the following - * the column number (0..M-1) of the first position to delete - * @param count the number of character to delete - * @return true: a join of 2 lines has been done + * @param lineNo the line number (0..N-1) of the first position to delete + * @param col -1: join the current line with the previous line. + * : join the current line with the following + * the column number (0..M-1) of the first position to delete + * @param count the number of character to delete + * @param withUndo true: prepares undo operation
+ * false: undo is impossible + * @return true: a join of 2 lines has been done */ -bool ReLines::remove(int line, int pos, int count) { +bool ReLines::removePart(int lineNo, int col, int count, bool withUndo) { bool rc = false; - if (line >= 0 && line < lineCount()){ - const QString& current = at(line); - if (pos == -1) { - rc = joinLines(line - 1); - } else if (pos == current.length()) { - rc = joinLines(line); - } else if (pos >= 0) { + if (lineNo >= 0 && lineNo < lineCount()) { + const QString& current = at(lineNo); + if (col == -1) { + if (lineNo > 0) { + if (withUndo) + storeJoin(lineNo - 1, at(lineNo - 1).length()); + rc = joinLines(lineNo - 1); + } + } else if (col == current.length()) { + if (withUndo) + storeJoin(lineNo, current.length()); + rc = joinLines(lineNo); + } else if (col >= 0) { int length = current.length(); - if (pos < length - 1) { - if (pos + count > length) - count = length - pos; - if (pos == 0) - replace(line, current.mid(count)); - else if (pos + count >= length) - replace(line, current.mid(0, pos)); + if (col < length - 1) { + if (col + count > length) + count = length - col; + if (withUndo) + storeRemovePart(lineNo, col, current.mid(col, count)); + if (col == 0) + replace(lineNo, current.mid(count)); + else if (col + count >= length) + replace(lineNo, current.mid(0, col)); else - replace(line, current.mid(0, pos) + current.mid(pos + count)); + replace(lineNo, + current.mid(0, col) + current.mid(col + count)); } } } @@ -175,45 +254,114 @@ bool ReLines::remove(int line, int pos, int count) { /** * Removes a given number of lines. * - * @param start the line number (0..N-1) of the first line to remove - * @param count the number of lines to delete + * @param start the line number (0..N-1) of the first line to remove + * @param count the number of lines to delete + * @param withUndo true: prepares undo operation
+ * false: undo is impossible */ - -void ReLines::removeLines(int start, int count) { +void ReLines::removeLines(int start, int count, bool withUndo) { if (start >= 0 && start < length()) { if (start + count > length()) count = length() - start; + if (withUndo) + storeRemoveLines(start, count, *this); for (int ix = start + count - 1; ix >= 0; ix--) removeAt(ix); } } +/** + * Splits a line at a given position into two lines. + * + * @param lineNo the line number (0..N-1) of line to split + * @param col the column number (0..M-1) of the split point. The character + * of this position will be the first in the next line + * @param withUndo true: prepares undo operation
+ * false: undo is impossible + */ +void ReLines::splitLine(int lineNo, int col, bool withUndo) { + if (lineNo >= 0 && lineNo < length() && col >= 0) { + QString current = at(lineNo); + if (withUndo) + storeSplit(lineNo, col); + int count = length(); + if (lineNo >= count - 1) { + if (col >= current.length()) + append(""); + else + append(current.mid(col)); + } else { + if (col >= current.length()) + insert(lineNo, ""); + else + insert(lineNo + 1, current.mid(col)); + } + replace(lineNo, current.mid(0, col)); + } +} +/** + * Rewinds the last change operation (insertion/deletion). + * + * @param lineNo OUT: the line number of the restored operation + * @param col OUT: the column of the restored operation + */ +void ReLines::undo(int& lineNo, int& col) { + if (m_list.length() > 0) { + UndoItem* item = pop(); + lineNo = item->m_lineNo; + col = item->m_position; + switch (item->m_type) { + case UIT_INSERT_PART: + removePart(item->m_lineNo, item->m_position, item->m_length, false); + break; + case UIT_INSERT_LINES: + removeLines(item->m_lineNo, item->m_length, false); + break; + case UIT_SPLIT: + joinLines(item->m_lineNo); + break; + case UIT_JOIN: + splitLine(item->m_lineNo, item->m_position, false); + break; + case UIT_REMOVE_LINES: + insertLines(item->m_lineNo, item->m_string, false); + break; + case UIT_REMOVE_PART: + insertPart(item->m_lineNo, item->m_position, item->m_string, false); + break; + default: + break; + } + delete item; + } +} + /** * Constructor. * * @param filename name of the file */ ReFile::ReFile(const QString& filename, bool readOnly, ReLogger* logger) : - ReLineSource(), - ReLines(), - m_endOfLine(), - m_filename(filename), - m_file(filename), - m_block(NULL), - // in 32-bit address space we allocate only 10 MByte, in 64-bit environments 100 GByte - m_blocksize( - sizeof(void*) <= 4 ? - 10 * 1024 * 1024ll : 0x100ll * 0x10000 * 0x10000), - m_blockOffset(0), - m_filesize(0), - m_startOfLine(NULL), - m_lineLength(0), - m_lineOffset(0), - m_currentLineNo(0), - m_maxLineLength(0x10000), - m_content(), - m_readOnly(readOnly), - m_logger(logger) { + ReLineSource(), + ReLines(), + m_endOfLine(), + m_filename(filename), + m_file(filename), + m_block(NULL), + // in 32-bit address space we allocate only 10 MByte, in 64-bit environments 100 GByte + m_blocksize( + sizeof(void*) <= 4 ? + 10 * 1024 * 1024ll : 0x100ll * 0x10000 * 0x10000), + m_blockOffset(0), + m_filesize(0), + m_startOfLine(NULL), + m_lineLength(0), + m_lineOffset(0), + m_currentLineNo(0), + m_maxLineLength(0x10000), + m_content(), + m_readOnly(readOnly), + m_logger(logger) { #if defined __linux__ setEndOfLine("\n"); #elif defined __WIN32__ @@ -247,7 +395,7 @@ int64_t ReFile::blocksize() const { * Frees the resources. */ void ReFile::close() { - clear(); + ReFile::clearUndo(); m_file.close(); } @@ -262,15 +410,15 @@ QString ReFile::filename() const { /** * Finds the next line with the string. * - * @param toFind the string to find "" or the pattern matching the result - * @param ignoreCase true: the search is case insensitive - * @param lineNo OUT: 0 or the line number - * @param line OUT: "" or the found line - * @return true: a line has been found
- * false: a line has not been found + * @param toFind the string to find "" or the pattern matching the result + * @param ignoreCase true: the search is case insensitive + * @param lineNo OUT: 0 or the line number + * @param line OUT: "" or the found line + * @return true: a line has been found
+ * false: a line has not been found */ bool ReFile::findLine(const char* toFind, bool ignoreCase, int& lineNo, - QString* line) { + QString* line) { bool rc = false; int length; int sourceLength = strlen(toFind); @@ -281,14 +429,14 @@ bool ReFile::findLine(const char* toFind, bool ignoreCase, int& lineNo, const char* ptr = start; int restLength = length - sourceLength + 1; while (restLength > 0 - && (ptr = reinterpret_cast( - ignoreCase ? - memchr(start, first, restLength) : - memichr(start, first, restLength))) != NULL) { + && (ptr = reinterpret_cast( + ignoreCase ? + memchr(start, first, restLength) : + memichr(start, first, restLength))) != NULL) { if (( - ignoreCase ? - _memicmp(ptr, toFind, sourceLength) : - memcmp(ptr, toFind, sourceLength)) == 0) { + ignoreCase ? + _memicmp(ptr, toFind, sourceLength) : + memcmp(ptr, toFind, sourceLength)) == 0) { rc = true; lineNo = m_currentLineNo; QByteArray buffer(m_startOfLine, m_lineLength); @@ -319,8 +467,8 @@ bool ReFile::findLine(const char* toFind, bool ignoreCase, int& lineNo, * false: a line has not been found */ bool ReFile::findLine(const QString& includePattern, bool includeIsRegExpr, - bool includeIgnoreCase, const QString& excludePattern, - bool excludeIsRegExpr, bool excludeIgnoreCase, int& lineNo, QString* line) { + bool includeIgnoreCase, const QString& excludePattern, + bool excludeIsRegExpr, bool excludeIgnoreCase, int& lineNo, QString* line) { bool rc = false; if (line != NULL) *line = ""; @@ -364,9 +512,9 @@ char* ReFile::nextLine(int& length) { if (m_currentLineNo == 65639) m_currentLineNo += 0; rc = m_startOfLine = remap(m_lineOffset += m_lineLength, - m_maxLineLength, lineLength); + m_maxLineLength, lineLength); const char* ptr = reinterpret_cast(memchr(rc, '\n', - lineLength)); + lineLength)); if (ptr != NULL) lineLength = ptr - rc + 1; length = m_lineLength = lineLength; @@ -488,7 +636,7 @@ char* ReFile::remap(int64_t offset, int size, int& length) { size = m_filesize - offset; // Note: size <= m_blocksize if (m_block != NULL && offset >= m_blockOffset - && offset + size <= m_blockOffset + m_blocksize) { + && offset + size <= m_blockOffset + m_blocksize) { // new block is inside the internal block: // no remapping needed rc = m_block + (offset - m_blockOffset); @@ -503,7 +651,7 @@ char* ReFile::remap(int64_t offset, int size, int& length) { if (m_block != NULL) m_file.unmap(reinterpret_cast(m_block)); m_block = reinterpret_cast(m_file.map(m_blockOffset, - m_blocksize)); + m_blocksize)); rc = m_block + (offset - m_blockOffset); length = m_blocksize - (rc - m_block); if (length > size) @@ -554,7 +702,7 @@ void ReFile::setFilename(const QString &filename) { * @return the name of an existing directory */ QByteArray ReFile::tempDir(const char* node, const char* parent, - bool withSeparator) { + bool withSeparator) { #if defined __linux__ QByteArray temp("/tmp"); static const char* firstVar = "TMP"; @@ -601,7 +749,7 @@ QByteArray ReFile::tempDir(const char* node, const char* parent, * @return the full name of a temporary file */ QByteArray ReFile::tempFile(const char* node, const char* parent, - bool deleteIfExists) { + bool deleteIfExists) { QByteArray rc(tempDir(parent)); if (!rc.endsWith('/')) rc += '/'; @@ -615,9 +763,9 @@ QByteArray ReFile::tempFile(const char* node, const char* parent, /** * Reads the content of the file into the line list. * - * @param filename the full name of the file. If "" the internal name will be used - * @return truesuccess
- * falsefile not readable + * @param filename the full name of the file. If "" the internal name will be used + * @return truesuccess
+ * falsefile not readable */ bool ReFile::write(const QString& filename) { bool rc = false; @@ -649,7 +797,7 @@ bool ReFile::write(const QString& filename) { * @param mode file write mode: "w" (write) or "a" (append) */ void ReFile::writeToFile(const char* filename, const char* content, - size_t contentLength, const char* mode) { + size_t contentLength, const char* mode) { FILE* fp = fopen(filename, mode); if (fp != NULL) { if (contentLength == (size_t) - 1) @@ -659,3 +807,184 @@ void ReFile::writeToFile(const char* filename, const char* content, } } +/** + * Constructor. + */ +ReUndoList::ReUndoList() : + m_list(), + m_current(NULL), + m_lastLine(-1), + m_lastPosition(-1), + m_maxUndoSize(10 * 1024 * 1024), + m_currentUndoSize(0) { +} + +/** + * Destructor. + */ +ReUndoList::~ReUndoList() { + clearUndo(); +} + +/** + * Frees the resources. + */ +void ReUndoList::clearUndo() { + for (int ii = 0; ii < m_list.length(); ii++) { + delete m_list.at(ii); + } + m_list.clear(); +} + +/** + * Returns the current maximum size of the undo information. + * + * @return the maximum size of the undo information + */ +qint64 ReUndoList::maxUndoSize() const { + return m_maxUndoSize; +} +/** + * Prepares the undo operation of an insertion in a given lineNo. + * + * @param line the line number of the the first line to insert + * @param count the number of lines to insert + */ +void ReUndoList::storeInsertLines(int lineNo, int count) { + UndoItem* item = new UndoItem(); + item->m_type = UIT_INSERT_LINES; + item->m_lineNo = lineNo; + item->m_position = 0; + item->m_length = count; + item->m_isPart = false; + m_list.append(item); + checkSummarySize(0); + m_currentUndoSize += (qint64) sizeof(*item); +} + +/** + * Prepares the undo operation of an insertion in a given line. + * + * @param lineNo the line number of the insertion point + * @param col the index of the insertion point in the line + * @param count the number of chars has been inserted + */ +void ReUndoList::storeInsertPart(int lineNo, int col, int count) { + UndoItem* item = new UndoItem(); + item->m_type = UIT_INSERT_PART; + item->m_lineNo = lineNo; + item->m_position = col; + item->m_length = count; + item->m_isPart = false; + m_list.append(item); + checkSummarySize(0); + m_currentUndoSize += (qint64) sizeof(*item); +} + +/** + * Prepares the undo operation of a join of two lines. + * + * @param lineNo the number of the first line to join. The second is line+1 + * @param length the length of the first line + */ +void ReUndoList::storeJoin(int lineNo, int length) { + UndoItem* item = new UndoItem(); + item->m_type = UIT_JOIN; + item->m_lineNo = lineNo; + item->m_position = length; + item->m_length = 0; + item->m_isPart = false; + m_list.append(item); + checkSummarySize(0); + m_currentUndoSize += (qint64) sizeof(*item); +} + +/** + * Prepares the undo operation of the deletion of a part of a line. + * + * @param lineNo the line number of the deletion point + * @param col the index of the deletion point in the line + * @param string the text which is deleted + */ +void ReUndoList::storeRemovePart(int lineNo, int col, const QString& string) { + UndoItem* item = new UndoItem(); + item->m_type = UIT_REMOVE_PART; + item->m_lineNo = lineNo; + item->m_position = col; + item->m_length = string.length(); + item->m_string = string; + item->m_isPart = false; + m_list.append(item); +} + +/** + * Sets the maximum size of the undo information. + * + * If the undo info exceeds this value the oldest infos will be removed. + * + * @param maxUndoSize the new maximum size of undo information + */ +void ReUndoList::setMaxUndoSize(qint64 maxUndoSize) { + if (maxUndoSize < (qint64) sizeof(UndoItem) + 1) + maxUndoSize = (qint64) sizeof(UndoItem) + 1; + m_maxUndoSize = maxUndoSize; +} + +/** + * Prepares the undo operation of a line split. + * + * @param lineNo the line number of line to split + * @param col the index of the split point. The line will be split behind + */ +void ReUndoList::storeSplit(int lineNo, int col) { + UndoItem* item = new UndoItem(); + item->m_type = UIT_SPLIT; + item->m_lineNo = lineNo; + item->m_position = col; + item->m_length = 0; + item->m_isPart = false; + m_list.append(item); + checkSummarySize(0); + m_currentUndoSize += (qint64) sizeof(*item); +} + +/** + * Prepares the undo operation of some lines. + * @param lineNo the number of the first line to remove + * @param count the number of lines to remove + * @param list the list containing the lines for extracting the line content + */ +void ReUndoList::storeRemoveLines(int lineNo, int count, + const QStringList& list) { + qint64 size = 0; + // Calculate the additional space + for (int ii = lineNo + count - 1; ii >= lineNo; ii--) + // +1: the newline + size += list.at(ii).length() + 1; + //@ToDo: handle more than 2**31 summary length + if (checkSummarySize(size)) { + assert(size <= INT_MAX); + UndoItem* item = new UndoItem(); + item->m_type = UIT_REMOVE_LINES; + item->m_lineNo = lineNo; + item->m_position = 0; + item->m_length = count; + item->m_string.reserve(size); + for (int ii = lineNo; ii < lineNo + count; ii++) + item->m_string.append(list.at(ii)).append('\n'); + m_list.append(item); + } +} + +/** + * Returns the last element of the undo item and remove it from the list. + * + * Note: Do not forget to free the returned item! + * + * @return the last element of the list + */ +ReUndoList::UndoItem* ReUndoList::pop() { + ReUndoList::UndoItem* item = m_list.last(); + m_list.removeLast(); + return item; +} diff --git a/base/ReFile.hpp b/base/ReFile.hpp index 2f4efcf..5123c68 100644 --- a/base/ReFile.hpp +++ b/base/ReFile.hpp @@ -12,17 +12,67 @@ #ifndef REFILE_HPP #define REFILE_HPP +class ReUndoList { +public: + enum UndoItemType { + UIT_UNDEF, + UIT_INSERT_PART, + UIT_INSERT_LINES, + UIT_JOIN, + UIT_REMOVE_PART, + UIT_REMOVE_LINES, + UIT_SPLIT, + }; + + class UndoItem { + public: + UndoItemType m_type; + int m_lineNo; + int m_position; + int m_length; + QString m_string; + ///@ true: the previous item belongs to this item (transaction) + bool m_isPart; + }; + +public: + ReUndoList(); + ~ReUndoList(); +public: + bool checkSummarySize(qint64 stringSize); + void clearUndo(); + qint64 maxUndoSize() const; + UndoItem* pop(); + void setMaxUndoSize(qint64 maxUndoSize); + void storeInsertPart(int lineNo, int pos, int count); + void storeInsertLines(int lineNo, int count); + void storeJoin(int lineNo, int length); + void storeRemovePart(int lineNo, int pos, const QString& string); + void storeRemoveLines(int lineNo, int count, const QStringList& list); + void storeSplit(int lineNo, int pos); +protected: + QList m_list; + UndoItem* m_current; + int m_lastLine; + int m_lastPosition; + qint64 m_maxUndoSize; + qint64 m_currentUndoSize; +}; + /** * Manages a list of lines. * * The lines will be stored without line terminators, e.g. '\n'. */ -class ReLines: protected QStringList { +class ReLines: public ReUndoList, protected QStringList { public: ReLines(); virtual ~ReLines(); public: - void insertText(int line, int col, const QString& text); + void clear(); + void insertLines(int lineNo, const QString& text, bool withUndo); + void insertPart(int lineNo, int col, const QString& text, bool withUndo); + void insertText(int lineNo, int col, const QString& text); bool joinLines(int first); /** Returns a line at a given position. * @param index the index of the line (0..N-1) @@ -39,8 +89,11 @@ public: int lineCount() const { return length(); } - virtual bool remove(int line, int pos, int count); - virtual void removeLines(int start, int count); + virtual bool removePart(int lineNo, int pos, int count, bool withUndo); + virtual void removeLines(int start, int count, bool withUndo); + void splitLine(int lineNo, int col, bool withUndo); + virtual void undo(int& lineNo, int& col); + protected: QString m_empty; }; @@ -53,7 +106,7 @@ public: class ReFile: public ReLineSource, public ReLines { public: ReFile(const QString& filename, bool readOnly = true, ReLogger* logger = - NULL); + NULL); ~ReFile(); public: int64_t blocksize() const; @@ -73,11 +126,11 @@ public: } QString filename() const; bool findLine(const char* toFind, bool ignoreCase, int& lineNo, - QString* line); + QString* line); bool findLine(const QString& includePattern, bool includeIsRegExpr, - bool includeIgnoreCase, const QString& excludePattern, - bool excludeIsRegExpr, bool excludeIgnoreCase, int& lineNo, - QString* line); + bool includeIgnoreCase, const QString& excludePattern, + bool excludeIsRegExpr, bool excludeIgnoreCase, int& lineNo, + QString* line); virtual int hasMoreLines(int index); char* nextLine(int& length); char* previousLine(int& length); @@ -96,12 +149,12 @@ public: public: static QByteArray tempDir(const char* node, const char* parent = NULL, - bool withSeparator = true); + bool withSeparator = true); static QByteArray tempFile(const char* node, const char* parent = NULL, - bool deleteIfExists = true); + bool deleteIfExists = true); static QByteArray& readFromFile(const char* filename, QByteArray& buffer); static void writeToFile(const char* filename, const char* content, - size_t contentLength = (size_t) - 1, const char* mode = "w"); + size_t contentLength = (size_t) - 1, const char* mode = "w"); private: QByteArray m_endOfLine; diff --git a/base/ReQStringUtil.cpp b/base/ReQStringUtil.cpp index 3494781..5773e1b 100644 --- a/base/ReQStringUtil.cpp +++ b/base/ReQStringUtil.cpp @@ -34,6 +34,25 @@ ReString ReQStringUtil::chomp(const ReString& text) { return last == text.length() - 1 ? text : text.mid(0, last + 1); } +/** + * Counts the occurencies of a character in a string. + * + * @param value string to inspect + * @param toFind character to find + * @param start first index to search + * + * @return the count of occurrencies of the character + */ +int ReQStringUtil::countOf(const QString& value, QChar toFind, int start) { + int rc = 0; + if (start >= 0) { + for (int ix = start; ix < value.length(); ix++) + if (value.at(ix) == toFind) + rc++; + } + return rc; +} + /** * Tests whether a given character is the last of the string and append it if not. * diff --git a/base/ReQStringUtil.hpp b/base/ReQStringUtil.hpp index c6135ee..2a860c6 100644 --- a/base/ReQStringUtil.hpp +++ b/base/ReQStringUtil.hpp @@ -15,6 +15,7 @@ class ReQStringUtil { public: static ReString chomp(const ReString& text); + static int countOf(const QString& value, QChar toFind, int start = 0); static QString& ensureLastChar(QString& value, QChar lastChar); static ReString extensionOf(const ReString& filename); static int lengthOfDate(const ReString& text, int start = 0, QDate* value = diff --git a/cunit/cuReFile.cpp b/cunit/cuReFile.cpp index d750ec0..c89fe5f 100644 --- a/cunit/cuReFile.cpp +++ b/cunit/cuReFile.cpp @@ -132,6 +132,7 @@ public: QByteArray fn2(ReFile::tempFile("test2.txt", "cuReFile", true)); file.write(fn2); file.close(); + file.clear(); checkEqu(0, file.lineCount()); ReFile file2(fn2, false); checkEqu(3, file2.lineCount()); @@ -189,39 +190,192 @@ public: checkEqu("ABCDE", lines.lineAt(2)); // at line start: - lines.remove(0, 0, 2); + lines.removePart(0, 0, 2, true); checkEqu(3, lines.lineCount()); checkEqu("3", lines.lineAt(0)); checkEqu("abcdefg", lines.lineAt(1)); checkEqu("ABCDE", lines.lineAt(2)); // at line end (precisely): - lines.remove(1, 5, 2); + lines.removePart(1, 5, 2, true); checkEqu(3, lines.lineCount()); checkEqu("3", lines.lineAt(0)); checkEqu("abcde", lines.lineAt(1)); checkEqu("ABCDE", lines.lineAt(2)); // at line end (too many chars): - lines.remove(1, 3, 99); + lines.removePart(1, 3, 99, true); checkEqu(3, lines.lineCount()); checkEqu("3", lines.lineAt(0)); checkEqu("abc", lines.lineAt(1)); checkEqu("ABCDE", lines.lineAt(2)); // no remove because of wrong arguments: - lines.remove(-1, 3, 1); - lines.remove(0, 1, 1); - lines.remove(3, 1, 1); + lines.removePart(-1, 3, 1, true); + checkEqu(3, lines.lineCount()); + lines.removePart(0, 2, 1, true); + checkEqu(3, lines.lineCount()); + lines.removePart(3, 1, 1, true); checkEqu(3, lines.lineCount()); checkEqu("3", lines.lineAt(0)); checkEqu("abc", lines.lineAt(1)); checkEqu("ABCDE", lines.lineAt(2)); } + void testRelLinesInsertLines() { + ReLines lines; + // inserts into an empty instance: + lines.insertLines(0, QString("123\nline2-abc\n"), true); + checkEqu(2, lines.lineCount()); + checkEqu("123", lines.lineAt(0)); + checkEqu("line2-abc", lines.lineAt(1)); + // inserts at first line, one line: + lines.insertLines(0, QString("line-0"), true); + checkEqu(3, lines.lineCount()); + checkEqu("line-0", lines.lineAt(0)); + checkEqu("123", lines.lineAt(1)); + checkEqu("line2-abc", lines.lineAt(2)); + // inserts in the middle, 2 lines, no '\n' at the end + lines.insertLines(1, QString("BCDE\nCDEF"), true); + checkEqu(5, lines.lineCount()); + checkEqu("line-0", lines.lineAt(0)); + checkEqu("BCDE", lines.lineAt(1)); + checkEqu("CDEF", lines.lineAt(2)); + checkEqu("123", lines.lineAt(3)); + checkEqu("line2-abc", lines.lineAt(4)); + // appends at the end, one line, ending with '\n': + lines.insertLines(6, QString("xyz\n"), true); + checkEqu(6, lines.lineCount()); + checkEqu("line-0", lines.lineAt(0)); + checkEqu("BCDE", lines.lineAt(1)); + checkEqu("CDEF", lines.lineAt(2)); + checkEqu("123", lines.lineAt(3)); + checkEqu("line2-abc", lines.lineAt(4)); + checkEqu("xyz", lines.lineAt(5)); + + // lineno outside: + lines.insertLines(-1, QString("bad\n"), true); + checkEqu(6, lines.lineCount()); + lines.insertLines(9999, QString("last\n"), true); + checkEqu(7, lines.lineCount()); + checkEqu("last", lines.lineAt(6)); + } + void testRelLinesInsertPart() { + ReLines lines; + // inserts into an empty instance: + lines.insertLines(0, QString("123\nabc\nA"), true); + // first position: + lines.insertPart(0, 0, QString("x"), true); + checkEqu(3, lines.lineCount()); + checkEqu("x123", lines.lineAt(0)); + checkEqu("abc", lines.lineAt(1)); + checkEqu("A", lines.lineAt(2)); + // in the middle of the lines, in the middle of the line + lines.insertPart(1, 2, QString("YY"), true); + checkEqu(3, lines.lineCount()); + checkEqu("x123", lines.lineAt(0)); + checkEqu("abYYc", lines.lineAt(1)); + checkEqu("A", lines.lineAt(2)); + // in the middle of the lines, at the end of the line + lines.insertPart(1, 5, QString("!?!"), true); + checkEqu(3, lines.lineCount()); + checkEqu("x123", lines.lineAt(0)); + checkEqu("abYYc!?!", lines.lineAt(1)); + checkEqu("A", lines.lineAt(2)); + // last line: + lines.insertPart(2, 0, QString("xyz"), true); + checkEqu(3, lines.lineCount()); + checkEqu("x123", lines.lineAt(0)); + checkEqu("abYYc!?!", lines.lineAt(1)); + checkEqu("xyzA", lines.lineAt(2)); + + // outside of the line range: + lines.insertPart(-1, 0, QString("wrong"), true); + checkEqu(3, lines.lineCount()); + checkEqu("x123", lines.lineAt(0)); + checkEqu("abYYc!?!", lines.lineAt(1)); + checkEqu("xyzA", lines.lineAt(2)); + lines.insertPart(3, 0, QString("wrong"), true); + checkEqu(3, lines.lineCount()); + checkEqu("x123", lines.lineAt(0)); + checkEqu("abYYc!?!", lines.lineAt(1)); + checkEqu("xyzA", lines.lineAt(2)); + + // outside of the column range: + lines.insertPart(0, -1, QString("wrong"), true); + checkEqu(3, lines.lineCount()); + checkEqu("x123", lines.lineAt(0)); + checkEqu("abYYc!?!", lines.lineAt(1)); + checkEqu("xyzA", lines.lineAt(2)); + lines.insertPart(1, 99, QString("appended"), true); + checkEqu(3, lines.lineCount()); + checkEqu("x123", lines.lineAt(0)); + checkEqu("abYYc!?!appended", lines.lineAt(1)); + checkEqu("xyzA", lines.lineAt(2)); + } + void testRelLinesjoinLines() { + ReLines lines; + // inserts into an empty instance: + lines.insertLines(0, QString("123\nabc\nA\nB"), true); + checkEqu(4, lines.lineCount()); + + // inside the lines: + lines.joinLines(1); + checkEqu(3, lines.lineCount()); + checkEqu("123", lines.lineAt(0)); + checkEqu("abcA", lines.lineAt(1)); + checkEqu("B", lines.lineAt(2)); + // the last two lines: + lines.joinLines(1); + checkEqu(2, lines.lineCount()); + checkEqu("123", lines.lineAt(0)); + checkEqu("abcAB", lines.lineAt(1)); + // the first two lines: + lines.joinLines(0); + checkEqu(1, lines.lineCount()); + checkEqu("123abcAB", lines.lineAt(0)); + } + void testReLinesSplitLine() { + ReLines lines; + // inserts into an empty instance: + lines.insertLines(0, QString("123\nabcdefg"), true); + + // in the middle of the line: + lines.splitLine(0, 1, true); + checkEqu(3, lines.lineCount()); + checkEqu("1", lines.lineAt(0)); + checkEqu("23", lines.lineAt(1)); + checkEqu("abcdefg", lines.lineAt(2)); + // at the end of the line: + lines.splitLine(1, 2, true); + checkEqu(4, lines.lineCount()); + checkEqu("1", lines.lineAt(0)); + checkEqu("", lines.lineAt(1)); + checkEqu("23", lines.lineAt(2)); + checkEqu("abcdefg", lines.lineAt(3)); + + lines.clear(); + lines.insertLines(0, QString("12"), true); + // in the middle of the last line: + lines.splitLine(0, 1, true); + checkEqu(2, lines.lineCount()); + checkEqu("1", lines.lineAt(0)); + checkEqu("2", lines.lineAt(1)); + + // in the end of the last line: + lines.splitLine(1, 99, true); + checkEqu(3, lines.lineCount()); + checkEqu("1", lines.lineAt(0)); + checkEqu("2", lines.lineAt(1)); + checkEqu("", lines.lineAt(2)); + } virtual void run() { testReLinesInsert(); + testReLinesSplitLine(); + testRelLinesjoinLines(); + testRelLinesInsertPart(); + testRelLinesInsertLines(); testReLinesRemove(); testWritableFile(); testPerformance(); diff --git a/cunit/cuReQStringUtil.cpp b/cunit/cuReQStringUtil.cpp index 15d2d29..1aae6fa 100644 --- a/cunit/cuReQStringUtil.cpp +++ b/cunit/cuReQStringUtil.cpp @@ -22,6 +22,16 @@ public: } public: + void testCountOf() { + checkEqu(2, ReQStringUtil::countOf(QString("axbx"), 'x')); + checkEqu(2, ReQStringUtil::countOf(QString("axbx"), 'x', 1)); + checkEqu(1, ReQStringUtil::countOf(QString("axbx"), 'x', 2)); + checkEqu(1, ReQStringUtil::countOf(QString("axbx"), 'x', 3)); + checkEqu(0, ReQStringUtil::countOf(QString("axbx"), 'x', 4)); + checkEqu(0, ReQStringUtil::countOf(QString("axbx"), 'x', 5)); + checkEqu(0, ReQStringUtil::countOf(QString("axbx"), 'x', -1)); + } + void testLengthOfUInt64() { quint64 value = -3; checkEqu(1, diff --git a/gui/ReEdit.cpp b/gui/ReEdit.cpp index 7ddbaec..f786174 100644 --- a/gui/ReEdit.cpp +++ b/gui/ReEdit.cpp @@ -204,7 +204,8 @@ void ReEdit::assignKeysStandard() { m_keyControl[Qt::Key_Delete] = EA_DEL_END_OF_LINE; m_keyShift[Qt::Key_Delete] = EA_DEL_BEGIN_OF_LINE; m_keyAlt[Qt::Key_Delete] = EA_DEL_LINE; - + m_keyControl[Qt::Key_Z] = EA_UNDO; + m_keyControlShift[Qt::Key_Z] = EA_REDO; } /** @@ -359,6 +360,11 @@ void ReEdit::editorAction(ReEdit::EditorAction action) { break; case EA_DEL_LINE: break; + case EA_UNDO: + m_lines->undo(m_cursorLine, m_cursorCol); + break; + case EA_REDO: + break; default: break; } @@ -394,8 +400,8 @@ void ReEdit::keyPressEvent(QKeyEvent* event) { switch (key) { case Qt::Key_Enter: case Qt::Key_Return: - m_lines->insertText(m_cursorLine, - columnToIndex(m_cursorCol) + 1, "\n"); + m_lines->splitLine(m_cursorLine, + columnToIndex(m_cursorCol) + 1, true); m_cursorCol = -1; m_cursorLine++; break; @@ -408,12 +414,12 @@ void ReEdit::keyPressEvent(QKeyEvent* event) { m_cursorCol = indexToColumn(m_lines->lineAt(m_cursorLine - 1) .length() - 1); } - if (m_lines->remove(m_cursorLine, columnToIndex(currentCol), 1)) + if (m_lines->removePart(m_cursorLine, columnToIndex(currentCol), 1, true)) m_cursorLine = max(0, m_cursorLine - 1); break; } case Qt::Key_Delete: - m_lines->remove(m_cursorLine, columnToIndex(m_cursorCol) + 1, 1); + m_lines->removePart(m_cursorLine, columnToIndex(m_cursorCol) + 1, 1, true); break; default: m_lines->insertText(m_cursorLine, diff --git a/gui/ReEdit.hpp b/gui/ReEdit.hpp index 49e4d0d..81d6a6c 100644 --- a/gui/ReEdit.hpp +++ b/gui/ReEdit.hpp @@ -289,6 +289,8 @@ public: EA_DEL_END_OF_LINE, EA_DEL_BEGIN_OF_LINE, EA_DEL_LINE, + EA_UNDO, + EA_REDO, }; public: explicit ReEdit(QWidget *parent = 0); -- 2.39.5