diff --git a/src/buffer/katetextblock.cpp b/src/buffer/katetextblock.cpp index 980cf3c7..3d2465fa 100644 --- a/src/buffer/katetextblock.cpp +++ b/src/buffer/katetextblock.cpp @@ -1,725 +1,746 @@ /* This file is part of the Kate project. * * Copyright (C) 2010 Christoph Cullmann * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "katetextblock.h" #include "katetextbuffer.h" +#include + namespace Kate { TextBlock::TextBlock(TextBuffer *buffer, int startLine) : m_buffer(buffer) , m_startLine(startLine) { // reserve the block size m_lines.reserve(m_buffer->m_blockSize); } TextBlock::~TextBlock() { // blocks should be empty before they are deleted! Q_ASSERT(m_lines.empty()); Q_ASSERT(m_cursors.empty()); // it only is a hint for ranges for this block, not the storage of them } void TextBlock::setStartLine(int startLine) { // allow only valid lines Q_ASSERT(startLine >= 0); Q_ASSERT(startLine < m_buffer->lines()); m_startLine = startLine; } TextLine TextBlock::line(int line) const { // right input Q_ASSERT(line >= startLine()); // calc internal line line = line - startLine(); // in range Q_ASSERT(line < m_lines.size()); // get text line return m_lines.at(line); } void TextBlock::appendLine(const QString &textOfLine) { m_lines.append(TextLine::create(textOfLine)); } void TextBlock::clearLines() { m_lines.clear(); } void TextBlock::text(QString &text) const { // combine all lines for (int i = 0; i < m_lines.size(); ++i) { // not first line, insert \n if (i > 0 || startLine() > 0) { text.append(QLatin1Char('\n')); } text.append(m_lines.at(i)->text()); } } void TextBlock::wrapLine(const KTextEditor::Cursor &position, int fixStartLinesStartIndex) { // calc internal line int line = position.line() - startLine(); // get text QString &text = m_lines.at(line)->textReadWrite(); // check if valid column Q_ASSERT(position.column() >= 0); Q_ASSERT(position.column() <= text.size()); // create new line and insert it m_lines.insert(m_lines.begin() + line + 1, TextLine(new TextLineData())); // cases for modification: // 1. line is wrapped in the middle // 2. if empty line is wrapped, mark new line as modified // 3. line-to-be-wrapped is already modified if (position.column() > 0 || text.size() == 0 || m_lines.at(line)->markedAsModified()) { m_lines.at(line + 1)->markAsModified(true); } else if (m_lines.at(line)->markedAsSavedOnDisk()) { m_lines.at(line + 1)->markAsSavedOnDisk(true); } // perhaps remove some text from previous line and append it if (position.column() < text.size()) { // text from old line moved first to new one m_lines.at(line + 1)->textReadWrite() = text.right(text.size() - position.column()); // now remove wrapped text from old line text.chop(text.size() - position.column()); // mark line as modified m_lines.at(line)->markAsModified(true); } /** * fix all start lines * we need to do this NOW, else the range update will FAIL! * bug 313759 */ m_buffer->fixStartLines(fixStartLinesStartIndex); /** * notify the text history */ m_buffer->history().wrapLine(position); /** * cursor and range handling below */ // no cursors will leave or join this block // no cursors in this block, no work to do.. if (m_cursors.empty()) { return; } // move all cursors on the line which has the text inserted - QSet changedRanges; + // remember all ranges modified, optimize for the standard case of a few ranges + QVarLengthArray changedRanges; for (TextCursor *cursor : qAsConst(m_cursors)) { // skip cursors on lines in front of the wrapped one! if (cursor->lineInBlock() < line) { continue; } // either this is simple, line behind the wrapped one if (cursor->lineInBlock() > line) { // patch line of cursor cursor->m_line++; } // this is the wrapped line else { // skip cursors with too small column if (cursor->column() <= position.column()) { if (cursor->column() < position.column() || !cursor->m_moveOnInsert) { continue; } } // move cursor // patch line of cursor cursor->m_line++; // patch column cursor->m_column -= position.column(); } - // remember range, if any - if (cursor->kateRange()) { - changedRanges.insert(cursor->kateRange()); + // remember range, if any, avoid double insert + auto range = cursor->kateRange(); + if (range && !range->isValidityCheckRequired()) { + range->setValidityCheckRequired(); + changedRanges.push_back(range); } } // we might need to invalidate ranges or notify about their changes // checkValidity might trigger delete of the range! for (TextRange *range : qAsConst(changedRanges)) { range->checkValidity(); } } void TextBlock::unwrapLine(int line, TextBlock *previousBlock, int fixStartLinesStartIndex) { // calc internal line line = line - startLine(); // two possiblities: either first line of this block or later line if (line == 0) { // we need previous block with at least one line Q_ASSERT(previousBlock); Q_ASSERT(previousBlock->lines() > 0); // move last line of previous block to this one, might result in empty block TextLine oldFirst = m_lines.at(0); int lastLineOfPreviousBlock = previousBlock->lines() - 1; TextLine newFirst = previousBlock->m_lines.last(); m_lines[0] = newFirst; previousBlock->m_lines.erase(previousBlock->m_lines.begin() + (previousBlock->lines() - 1)); const int oldSizeOfPreviousLine = newFirst->text().size(); if (oldFirst->length() > 0) { // append text newFirst->textReadWrite().append(oldFirst->text()); // mark line as modified, since text was appended newFirst->markAsModified(true); } // patch startLine of this block --m_startLine; /** * fix all start lines * we need to do this NOW, else the range update will FAIL! * bug 313759 */ m_buffer->fixStartLines(fixStartLinesStartIndex); /** * notify the text history in advance */ m_buffer->history().unwrapLine(startLine() + line, oldSizeOfPreviousLine); /** * cursor and range handling below */ // no cursors in this block and the previous one, no work to do.. if (m_cursors.empty() && previousBlock->m_cursors.empty()) { return; } // move all cursors because of the unwrapped line - QSet changedRanges; + // remember all ranges modified, optimize for the standard case of a few ranges + QVarLengthArray changedRanges; for (TextCursor *cursor : qAsConst(m_cursors)) { // this is the unwrapped line if (cursor->lineInBlock() == 0) { // patch column cursor->m_column += oldSizeOfPreviousLine; - // remember range, if any - if (cursor->kateRange()) { - changedRanges.insert(cursor->kateRange()); + // remember range, if any, avoid double insert + auto range = cursor->kateRange(); + if (range && !range->isValidityCheckRequired()) { + range->setValidityCheckRequired(); + changedRanges.push_back(range); } } } // move cursors of the moved line from previous block to this block now QSet newPreviousCursors; for (TextCursor *cursor : qAsConst(previousBlock->m_cursors)) { if (cursor->lineInBlock() == lastLineOfPreviousBlock) { cursor->m_line = 0; cursor->m_block = this; m_cursors.insert(cursor); - // remember range, if any - if (cursor->kateRange()) { - changedRanges.insert(cursor->kateRange()); + // remember range, if any, avoid double insert + auto range = cursor->kateRange(); + if (range && !range->isValidityCheckRequired()) { + range->setValidityCheckRequired(); + changedRanges.push_back(range); } } else { newPreviousCursors.insert(cursor); } } previousBlock->m_cursors = newPreviousCursors; // fixup the ranges that might be effected, because they moved from last line to this block // we might need to invalidate ranges or notify about their changes // checkValidity might trigger delete of the range! for (TextRange *range : qAsConst(changedRanges)) { // update both blocks updateRange(range); previousBlock->updateRange(range); // afterwards check validity, might delete this range! range->checkValidity(); } // be done return; } // easy: just move text to previous line and remove current one const int oldSizeOfPreviousLine = m_lines.at(line - 1)->length(); const int sizeOfCurrentLine = m_lines.at(line)->length(); if (sizeOfCurrentLine > 0) { m_lines.at(line - 1)->textReadWrite().append(m_lines.at(line)->text()); } const bool lineChanged = (oldSizeOfPreviousLine > 0 && m_lines.at(line - 1)->markedAsModified()) || (sizeOfCurrentLine > 0 && (oldSizeOfPreviousLine > 0 || m_lines.at(line)->markedAsModified())); m_lines.at(line - 1)->markAsModified(lineChanged); if (oldSizeOfPreviousLine == 0 && m_lines.at(line)->markedAsSavedOnDisk()) { m_lines.at(line - 1)->markAsSavedOnDisk(true); } m_lines.erase(m_lines.begin() + line); /** * fix all start lines * we need to do this NOW, else the range update will FAIL! * bug 313759 */ m_buffer->fixStartLines(fixStartLinesStartIndex); /** * notify the text history in advance */ m_buffer->history().unwrapLine(startLine() + line, oldSizeOfPreviousLine); /** * cursor and range handling below */ // no cursors in this block, no work to do.. if (m_cursors.empty()) { return; } // move all cursors because of the unwrapped line - QSet changedRanges; + // remember all ranges modified, optimize for the standard case of a few ranges + QVarLengthArray changedRanges; for (TextCursor *cursor : qAsConst(m_cursors)) { // skip cursors in lines in front of removed one if (cursor->lineInBlock() < line) { continue; } // this is the unwrapped line if (cursor->lineInBlock() == line) { // patch column cursor->m_column += oldSizeOfPreviousLine; } // patch line of cursor cursor->m_line--; - // remember range, if any - if (cursor->kateRange()) { - changedRanges.insert(cursor->kateRange()); + // remember range, if any, avoid double insert + auto range = cursor->kateRange(); + if (range && !range->isValidityCheckRequired()) { + range->setValidityCheckRequired(); + changedRanges.push_back(range); } } // we might need to invalidate ranges or notify about their changes // checkValidity might trigger delete of the range! for (TextRange *range : qAsConst(changedRanges)) { range->checkValidity(); } } void TextBlock::insertText(const KTextEditor::Cursor &position, const QString &text) { // calc internal line int line = position.line() - startLine(); // get text QString &textOfLine = m_lines.at(line)->textReadWrite(); int oldLength = textOfLine.size(); m_lines.at(line)->markAsModified(true); // check if valid column Q_ASSERT(position.column() >= 0); Q_ASSERT(position.column() <= textOfLine.size()); // insert text textOfLine.insert(position.column(), text); /** * notify the text history */ m_buffer->history().insertText(position, text.size(), oldLength); /** * cursor and range handling below */ // no cursors in this block, no work to do.. if (m_cursors.empty()) { return; } // move all cursors on the line which has the text inserted - QSet changedRanges; + // remember all ranges modified, optimize for the standard case of a few ranges + QVarLengthArray changedRanges; for (TextCursor *cursor : qAsConst(m_cursors)) { // skip cursors not on this line! if (cursor->lineInBlock() != line) { continue; } // skip cursors with too small column if (cursor->column() <= position.column()) { if (cursor->column() < position.column() || !cursor->m_moveOnInsert) { continue; } } // patch column of cursor if (cursor->m_column <= oldLength) { cursor->m_column += text.size(); } // special handling if cursor behind the real line, e.g. non-wrapping cursor in block selection mode else if (cursor->m_column < textOfLine.size()) { cursor->m_column = textOfLine.size(); } + // remember range, if any, avoid double insert // we only need to trigger checkValidity later if the range has feedback or might be invalidated - if (cursor->kateRange() && (cursor->kateRange()->feedback() || cursor->kateRange()->start().line() == cursor->kateRange()->end().line())) { - changedRanges.insert(cursor->kateRange()); + auto range = cursor->kateRange(); + if (range && !range->isValidityCheckRequired() && (range->feedback() || range->start().line() == range->end().line())) { + range->setValidityCheckRequired(); + changedRanges.push_back(range); } } // we might need to invalidate ranges or notify about their changes // checkValidity might trigger delete of the range! for (TextRange *range : qAsConst(changedRanges)) { range->checkValidity(); } } void TextBlock::removeText(const KTextEditor::Range &range, QString &removedText) { // calc internal line int line = range.start().line() - startLine(); // get text QString &textOfLine = m_lines.at(line)->textReadWrite(); int oldLength = textOfLine.size(); // check if valid column Q_ASSERT(range.start().column() >= 0); Q_ASSERT(range.start().column() <= textOfLine.size()); Q_ASSERT(range.end().column() >= 0); Q_ASSERT(range.end().column() <= textOfLine.size()); // get text which will be removed removedText = textOfLine.mid(range.start().column(), range.end().column() - range.start().column()); // remove text textOfLine.remove(range.start().column(), range.end().column() - range.start().column()); m_lines.at(line)->markAsModified(true); /** * notify the text history */ m_buffer->history().removeText(range, oldLength); /** * cursor and range handling below */ // no cursors in this block, no work to do.. if (m_cursors.empty()) { return; } // move all cursors on the line which has the text removed - QSet changedRanges; + // remember all ranges modified, optimize for the standard case of a few ranges + QVarLengthArray changedRanges; for (TextCursor *cursor : qAsConst(m_cursors)) { // skip cursors not on this line! if (cursor->lineInBlock() != line) { continue; } // skip cursors with too small column if (cursor->column() <= range.start().column()) { continue; } // patch column of cursor if (cursor->column() <= range.end().column()) { cursor->m_column = range.start().column(); } else { cursor->m_column -= (range.end().column() - range.start().column()); } + // remember range, if any, avoid double insert // we only need to trigger checkValidity later if the range has feedback or might be invalidated - if (cursor->kateRange() && (cursor->kateRange()->feedback() || cursor->kateRange()->start().line() == cursor->kateRange()->end().line())) { - changedRanges.insert(cursor->kateRange()); + auto range = cursor->kateRange(); + if (range && !range->isValidityCheckRequired() && (range->feedback() || range->start().line() == range->end().line())) { + range->setValidityCheckRequired(); + changedRanges.push_back(range); } } // we might need to invalidate ranges or notify about their changes // checkValidity might trigger delete of the range! for (TextRange *range : qAsConst(changedRanges)) { range->checkValidity(); } } void TextBlock::debugPrint(int blockIndex) const { // print all blocks for (int i = 0; i < m_lines.size(); ++i) printf("%4d - %4d : %4d : '%s'\n", blockIndex, startLine() + i , m_lines.at(i)->text().size(), qPrintable(m_lines.at(i)->text())); } TextBlock *TextBlock::splitBlock(int fromLine) { // half the block int linesOfNewBlock = lines() - fromLine; // create and insert new block TextBlock *newBlock = new TextBlock(m_buffer, startLine() + fromLine); // move lines newBlock->m_lines.reserve(linesOfNewBlock); for (int i = fromLine; i < m_lines.size(); ++i) { newBlock->m_lines.append(m_lines.at(i)); } m_lines.resize(fromLine); // move cursors QSet oldBlockSet; for (TextCursor *cursor : qAsConst(m_cursors)) { if (cursor->lineInBlock() >= fromLine) { cursor->m_line = cursor->lineInBlock() - fromLine; cursor->m_block = newBlock; newBlock->m_cursors.insert(cursor); } else { oldBlockSet.insert(cursor); } } m_cursors = oldBlockSet; // fix ALL ranges! const QList allRanges = m_uncachedRanges.toList() + m_cachedLineForRanges.keys(); for (TextRange *range : qAsConst(allRanges)) { // update both blocks updateRange(range); newBlock->updateRange(range); } // return the new generated block return newBlock; } void TextBlock::mergeBlock(TextBlock *targetBlock) { // move cursors, do this first, now still lines() count is correct for target for (TextCursor *cursor : qAsConst(m_cursors)) { cursor->m_line = cursor->lineInBlock() + targetBlock->lines(); cursor->m_block = targetBlock; targetBlock->m_cursors.insert(cursor); } m_cursors.clear(); // move lines targetBlock->m_lines.reserve(targetBlock->lines() + lines()); for (int i = 0; i < m_lines.size(); ++i) { targetBlock->m_lines.append(m_lines.at(i)); } m_lines.clear(); // fix ALL ranges! const QList allRanges = m_uncachedRanges.toList() + m_cachedLineForRanges.keys(); for (TextRange *range : qAsConst(allRanges)) { // update both blocks updateRange(range); targetBlock->updateRange(range); } } void TextBlock::deleteBlockContent() { // kill cursors, if not belonging to a range QSet copy = m_cursors; for (TextCursor *cursor : qAsConst(copy)) { if (!cursor->kateRange()) { delete cursor; } } // kill lines m_lines.clear(); } void TextBlock::clearBlockContent(TextBlock *targetBlock) { // move cursors, if not belonging to a range QSet copy = m_cursors; for (TextCursor *cursor : qAsConst(copy)) { if (!cursor->kateRange()) { cursor->m_column = 0; cursor->m_line = 0; cursor->m_block = targetBlock; targetBlock->m_cursors.insert(cursor); m_cursors.remove(cursor); } } // kill lines m_lines.clear(); } void TextBlock::markModifiedLinesAsSaved() { // mark all modified lines as saved for (int i = 0; i < m_lines.size(); ++i) { TextLine textLine = m_lines[i]; if (textLine->markedAsModified()) { textLine->markAsSavedOnDisk(true); } } } void TextBlock::updateRange(TextRange *range) { /** * get some simple facts about our nice range */ const int startLine = range->startInternal().lineInternal(); const int endLine = range->endInternal().lineInternal(); const bool isSingleLine = startLine == endLine; /** * perhaps remove range and be done */ if ((endLine < m_startLine) || (startLine >= (m_startLine + lines()))) { removeRange(range); return; } /** * The range is still a single-line range, and is still cached to the correct line. */ if (isSingleLine && m_cachedLineForRanges.contains(range) && (m_cachedLineForRanges.value(range) == startLine - m_startLine)) { return; } /** * The range is still a multi-line range, and is already in the correct set. */ if (!isSingleLine && m_uncachedRanges.contains(range)) { return; } /** * remove, if already there! */ removeRange(range); /** * simple case: multi-line range */ if (!isSingleLine) { /** * The range cannot be cached per line, as it spans multiple lines */ m_uncachedRanges.insert(range); return; } /** * The range is contained by a single line, put it into the line-cache */ const int lineOffset = startLine - m_startLine; /** * enlarge cache if needed */ if (m_cachedRangesForLine.size() <= lineOffset) { m_cachedRangesForLine.resize(lineOffset + 1); } /** * insert into mapping */ m_cachedRangesForLine[lineOffset].insert(range); m_cachedLineForRanges[range] = lineOffset; } void TextBlock::removeRange(TextRange *range) { /** * uncached range? remove it and be done */ if (m_uncachedRanges.remove(range)) { /** * must be only uncached! */ Q_ASSERT(!m_cachedLineForRanges.contains(range)); return; } /** * cached range? */ QHash::iterator it = m_cachedLineForRanges.find(range); if (it != m_cachedLineForRanges.end()) { /** * must be only cached! */ Q_ASSERT(!m_uncachedRanges.contains(range)); /** * query the range from cache, must be there */ Q_ASSERT(m_cachedRangesForLine.at(*it).contains(range)); /** * remove it and be done */ m_cachedRangesForLine[*it].remove(range); m_cachedLineForRanges.erase(it); return; } /** * else: range was not for this block, just do nothing, removeRange should be "safe" to use */ } } diff --git a/src/buffer/katetextrange.cpp b/src/buffer/katetextrange.cpp index b4403de0..30c29345 100644 --- a/src/buffer/katetextrange.cpp +++ b/src/buffer/katetextrange.cpp @@ -1,356 +1,361 @@ /* This file is part of the Kate project. * * Copyright (C) 2010 Christoph Cullmann * * Based on code of the SmartCursor/Range by: * Copyright (C) 2003-2005 Hamish Rodda * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "katetextrange.h" #include "katetextbuffer.h" namespace Kate { TextRange::TextRange(TextBuffer &buffer, const KTextEditor::Range &range, InsertBehaviors insertBehavior, EmptyBehavior emptyBehavior) : m_buffer(buffer) , m_start(buffer, this, range.start(), (insertBehavior &ExpandLeft) ? Kate::TextCursor::StayOnInsert : Kate::TextCursor::MoveOnInsert) , m_end(buffer, this, range.end(), (insertBehavior &ExpandRight) ? Kate::TextCursor::MoveOnInsert : Kate::TextCursor::StayOnInsert) , m_view(nullptr) , m_feedback(nullptr) , m_zDepth(0.0) , m_attributeOnlyForViews(false) , m_invalidateIfEmpty(emptyBehavior == InvalidateIfEmpty) { // remember this range in buffer m_buffer.m_ranges.insert(this); // check if range now invalid, there can happen no feedback, as m_feedback == 0 checkValidity(); } TextRange::~TextRange() { /** * reset feedback, don't want feedback during destruction */ m_feedback = nullptr; // remove range from m_ranges fixLookup(m_start.line(), m_end.line(), -1, -1); // remove this range from the buffer m_buffer.m_ranges.remove(this); /** * trigger update, if we have attribute * notify right view * here we can ignore feedback, even with feedback, we want none if the range is deleted! */ if (m_attribute) { m_buffer.notifyAboutRangeChange(m_view, m_start.line(), m_end.line(), true /* we have a attribute */); } } void TextRange::setInsertBehaviors(InsertBehaviors _insertBehaviors) { /** * nothing to do? */ if (_insertBehaviors == insertBehaviors()) { return; } /** * modify cursors */ m_start.setInsertBehavior((_insertBehaviors & ExpandLeft) ? KTextEditor::MovingCursor::StayOnInsert : KTextEditor::MovingCursor::MoveOnInsert); m_end.setInsertBehavior((_insertBehaviors & ExpandRight) ? KTextEditor::MovingCursor::MoveOnInsert : KTextEditor::MovingCursor::StayOnInsert); /** * notify world */ if (m_attribute || m_feedback) { m_buffer.notifyAboutRangeChange(m_view, m_start.line(), m_end.line(), true /* we have a attribute */); } } KTextEditor::MovingRange::InsertBehaviors TextRange::insertBehaviors() const { InsertBehaviors behaviors = DoNotExpand; if (m_start.insertBehavior() == KTextEditor::MovingCursor::StayOnInsert) { behaviors |= ExpandLeft; } if (m_end.insertBehavior() == KTextEditor::MovingCursor::MoveOnInsert) { behaviors |= ExpandRight; } return behaviors; } void TextRange::setEmptyBehavior(EmptyBehavior emptyBehavior) { /** * nothing to do? */ if (m_invalidateIfEmpty == (emptyBehavior == InvalidateIfEmpty)) { return; } /** * remember value */ m_invalidateIfEmpty = (emptyBehavior == InvalidateIfEmpty); /** * invalidate range? */ if (end() <= start()) { setRange(KTextEditor::Range::invalid()); } } void TextRange::setRange(const KTextEditor::Range &range) { // avoid work if nothing changed! if (range == toRange()) { return; } // remember old line range int oldStartLine = m_start.line(); int oldEndLine = m_end.line(); // change start and end cursor m_start.setPosition(range.start()); m_end.setPosition(range.end()); // check if range now invalid, don't emit feedback here, will be handled below // otherwise you can't delete ranges in feedback! checkValidity(oldStartLine, oldEndLine, false); // no attribute or feedback set, be done if (!m_attribute && !m_feedback) { return; } // get full range int startLineMin = oldStartLine; if (oldStartLine == -1 || (m_start.line() != -1 && m_start.line() < oldStartLine)) { startLineMin = m_start.line(); } int endLineMax = oldEndLine; if (oldEndLine == -1 || m_end.line() > oldEndLine) { endLineMax = m_end.line(); } /** * notify buffer about attribute change, it will propagate the changes * notify right view */ m_buffer.notifyAboutRangeChange(m_view, startLineMin, endLineMax, m_attribute); // perhaps need to notify stuff! if (m_feedback) { // do this last: may delete this range if (!toRange().isValid()) { m_feedback->rangeInvalid(this); } else if (toRange().isEmpty()) { m_feedback->rangeEmpty(this); } } } void TextRange::checkValidity(int oldStartLine, int oldEndLine, bool notifyAboutChange) { + /** + * in any case: reset the flag, to avoid multiple runs + */ + m_isCheckValidityRequired = false; + /** * check if any cursor is invalid or the range is zero size and it should be invalidated then */ if (!m_start.isValid() || !m_end.isValid() || (m_invalidateIfEmpty && m_end <= m_start)) { m_start.setPosition(-1, -1); m_end.setPosition(-1, -1); } /** * for ranges which are allowed to become empty, normalize them, if the end has moved to the front of the start */ if (!m_invalidateIfEmpty && m_end < m_start) { m_end.setPosition(m_start); } // fix lookup fixLookup(oldStartLine, oldEndLine, m_start.line(), m_end.line()); // perhaps need to notify stuff! if (notifyAboutChange && m_feedback) { m_buffer.notifyAboutRangeChange(m_view, m_start.line(), m_end.line(), false /* attribute not interesting here */); // do this last: may delete this range if (!toRange().isValid()) { m_feedback->rangeInvalid(this); } else if (toRange().isEmpty()) { m_feedback->rangeEmpty(this); } } } void TextRange::fixLookup(int oldStartLine, int oldEndLine, int startLine, int endLine) { // nothing changed? if (oldStartLine == startLine && oldEndLine == endLine) { return; } // now, not both can be invalid Q_ASSERT(oldStartLine >= 0 || startLine >= 0); Q_ASSERT(oldEndLine >= 0 || endLine >= 0); // get full range int startLineMin = oldStartLine; if (oldStartLine == -1 || (startLine != -1 && startLine < oldStartLine)) { startLineMin = startLine; } int endLineMax = oldEndLine; if (oldEndLine == -1 || endLine > oldEndLine) { endLineMax = endLine; } // get start block int blockIndex = m_buffer.blockForLine(startLineMin); Q_ASSERT(blockIndex >= 0); // remove this range from m_ranges for (; blockIndex < m_buffer.m_blocks.size(); ++blockIndex) { // get block TextBlock *block = m_buffer.m_blocks[blockIndex]; // either insert or remove range if ((endLine < block->startLine()) || (startLine >= (block->startLine() + block->lines()))) { block->removeRange(this); } else { block->updateRange(this); } // ok, reached end block if (endLineMax < (block->startLine() + block->lines())) { return; } } // we should not be here, really, then endLine is wrong Q_ASSERT(false); } void TextRange::setView(KTextEditor::View *view) { /** * nothing changes, nop */ if (view == m_view) { return; } /** * remember the new attribute */ m_view = view; /** * notify buffer about attribute change, it will propagate the changes * notify all views (can be optimized later) */ if (m_attribute || m_feedback) { m_buffer.notifyAboutRangeChange(nullptr, m_start.line(), m_end.line(), m_attribute); } } void TextRange::setAttribute(KTextEditor::Attribute::Ptr attribute) { /** * remember the new attribute */ m_attribute = attribute; /** * notify buffer about attribute change, it will propagate the changes * notify right view */ m_buffer.notifyAboutRangeChange(m_view, m_start.line(), m_end.line(), m_attribute); } void TextRange::setFeedback(KTextEditor::MovingRangeFeedback *feedback) { /** * nothing changes, nop */ if (feedback == m_feedback) { return; } /** * remember the new feedback object */ m_feedback = feedback; /** * notify buffer about feedback change, it will propagate the changes * notify right view */ m_buffer.notifyAboutRangeChange(m_view, m_start.line(), m_end.line(), m_attribute); } void TextRange::setAttributeOnlyForViews(bool onlyForViews) { /** * just set the value, no need to trigger updates, printing is not interruptable */ m_attributeOnlyForViews = onlyForViews; } void TextRange::setZDepth(qreal zDepth) { /** * nothing changes, nop */ if (zDepth == m_zDepth) { return; } /** * remember the new attribute */ m_zDepth = zDepth; /** * notify buffer about attribute change, it will propagate the changes */ if (m_attribute) { m_buffer.notifyAboutRangeChange(m_view, m_start.line(), m_end.line(), m_attribute); } } KTextEditor::Document *Kate::TextRange::document() const { return m_buffer.document(); } } diff --git a/src/buffer/katetextrange.h b/src/buffer/katetextrange.h index 5827c167..0d336273 100644 --- a/src/buffer/katetextrange.h +++ b/src/buffer/katetextrange.h @@ -1,366 +1,390 @@ /* This file is part of the Kate project. * * Copyright (C) 2010 Christoph Cullmann * * Based on code of the SmartCursor/Range by: * Copyright (C) 2003-2005 Hamish Rodda * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef KATE_TEXTRANGE_H #define KATE_TEXTRANGE_H #include #include #include #include #include "katetextcursor.h" namespace Kate { class TextBuffer; /** * Class representing a 'clever' text range. * It will automagically move if the text inside the buffer it belongs to is modified. * By intention no subclass of KTextEditor::Range, must be converted manually. * A TextRange is allowed to be empty. If you call setInvalidateIfEmpty(true), * a TextRange will become automatically invalid as soon as start() == end() * position holds. */ class KTEXTEDITOR_EXPORT TextRange : public KTextEditor::MovingRange { // this is a friend, block changes might invalidate ranges... friend class TextBlock; public: /** * Construct a text range. * A TextRange is not allowed to be empty, as soon as start == end position, it will become * automatically invalid! * @param buffer parent text buffer * @param range The initial text range assumed by the new range. * @param insertBehavior Define whether the range should expand when text is inserted adjacent to the range. * @param emptyBehavior Define whether the range should invalidate itself on becoming empty. */ TextRange(TextBuffer &buffer, const KTextEditor::Range &range, InsertBehaviors insertBehavior, EmptyBehavior emptyBehavior = AllowEmpty); /** * Destruct the text block */ ~TextRange() override; /** * Set insert behaviors. * @param insertBehaviors new insert behaviors */ void setInsertBehaviors(InsertBehaviors insertBehaviors) override; /** * Get current insert behaviors. * @return current insert behaviors */ InsertBehaviors insertBehaviors() const override; /** * Set if this range will invalidate itself if it becomes empty. * @param emptyBehavior behavior on becoming empty */ void setEmptyBehavior(EmptyBehavior emptyBehavior) override; /** * Will this range invalidate itself if it becomes empty? * @return behavior on becoming empty */ EmptyBehavior emptyBehavior() const override { return m_invalidateIfEmpty ? InvalidateIfEmpty : AllowEmpty; } /** * Gets the document to which this range is bound. * \return a pointer to the document */ KTextEditor::Document *document() const override; /** * Set the range of this range. * A TextRange is not allowed to be empty, as soon as start == end position, it will become * automatically invalid! * @param range new range for this clever range */ void setRange(const KTextEditor::Range &range) override; /** * \overload * Set the range of this range * A TextRange is not allowed to be empty, as soon as start == end position, it will become * automatically invalid! * @param start new start for this clever range * @param end new end for this clever range */ void setRange(const KTextEditor::Cursor &start, const KTextEditor::Cursor &end) { KTextEditor::MovingRange::setRange(start, end); } /** * Retrieve start cursor of this range, read-only. * @return start cursor */ const KTextEditor::MovingCursor &start() const override { return m_start; } /** * Non-virtual version of start(), which is faster. * @return start cursor */ const TextCursor &startInternal() const { return m_start; } /** * Retrieve end cursor of this range, read-only. * @return end cursor */ const KTextEditor::MovingCursor &end() const override { return m_end; } /** * Nonvirtual version of end(), which is faster. * @return end cursor */ const TextCursor &endInternal() const { return m_end; } /** * Convert this clever range into a dumb one. * @return normal range */ const KTextEditor::Range toRange() const { return KTextEditor::Range(start().toCursor(), end().toCursor()); } /** * Convert this clever range into a dumb one. Equal to toRange, allowing to use implicit conversion. * @return normal range */ operator KTextEditor::Range() const { return KTextEditor::Range(start().toCursor(), end().toCursor()); } /** * Gets the active view for this range. Might be already invalid, internally only used for pointer comparisons. * * \return a pointer to the active view */ KTextEditor::View *view() const override { return m_view; } /** * Sets the currently active view for this range. * This will trigger update of the relevant view parts, if the view changed. * Set view before the attribute, that will avoid not needed redraws. * * \param view View to assign to this range. If null, simply * removes the previous view. */ void setView(KTextEditor::View *view) override; /** * Gets the active Attribute for this range. * * \return a pointer to the active attribute */ KTextEditor::Attribute::Ptr attribute() const override { return m_attribute; } /** * \return whether a nonzero attribute is set. This is faster than checking attribute(), * because the reference-counting is omitted. */ bool hasAttribute() const { return m_attribute.constData(); } /** * Sets the currently active attribute for this range. * This will trigger update of the relevant view parts. * * \param attribute Attribute to assign to this range. If null, simply * removes the previous Attribute. */ void setAttribute(KTextEditor::Attribute::Ptr attribute) override; /** * Gets the active MovingRangeFeedback for this range. * * \return a pointer to the active MovingRangeFeedback */ KTextEditor::MovingRangeFeedback *feedback() const override { return m_feedback; } /** * Sets the currently active MovingRangeFeedback for this range. * This will trigger evaluation if feedback must be send again (for example if mouse is already inside range). * * \param feedback MovingRangeFeedback to assign to this range. If null, simply * removes the previous MovingRangeFeedback. */ void setFeedback(KTextEditor::MovingRangeFeedback *feedback) override; /** * Is this range's attribute only visible in views, not for example prints? * Default is false. * @return range visible only for views */ bool attributeOnlyForViews() const override { return m_attributeOnlyForViews; } /** * Set if this range's attribute is only visible in views, not for example prints. * @param onlyForViews attribute only valid for views */ void setAttributeOnlyForViews(bool onlyForViews) override; /** * Gets the current Z-depth of this range. * Ranges with smaller Z-depth than others will win during rendering. * Default is 0.0. * * \return current Z-depth of this range */ qreal zDepth() const override { return m_zDepth; } /** * Set the current Z-depth of this range. * Ranges with smaller Z-depth than others will win during rendering. * This will trigger update of the relevant view parts, if the depth changed. * Set depth before the attribute, that will avoid not needed redraws. * Default is 0.0. * * \param zDepth new Z-depth of this range */ void setZDepth(qreal zDepth) override; private: /** * no copy constructor, don't allow this to be copied. */ - TextRange(const TextRange &); + TextRange(const TextRange &) = delete; /** * no assignment operator, no copying around. */ - TextRange &operator= (const TextRange &); + TextRange &operator= (const TextRange &) = delete; /** * Check if range is valid, used by constructor and setRange. * If at least one cursor is invalid, both will set to invalid. * Same if range itself is invalid (start >= end). * * IMPORTANT: Notifications might need to deletion of this range! * * @param oldStartLine old start line of this range before changing of cursors, needed to add/remove range from m_ranges in blocks * @param oldEndLine old end line of this range * @param notifyAboutChange should feedback be emitted or not? */ void checkValidity(int oldStartLine = -1, int oldEndLine = -1, bool notifyAboutChange = true); /** * Add/Remove range from the lookup m_ranges hash of each block * @param oldStartLine old start line of this range before changing of cursors, needed to add/remove range from m_ranges in blocks * @param oldEndLine old end line of this range * @param startLine start line to start looking for the range to remove * @param endLine end line of this range */ void fixLookup(int oldStartLine, int oldEndLine, int startLine, int endLine); + /** + * Mark this range for later validity checking. + */ + void setValidityCheckRequired() + { + m_isCheckValidityRequired = true; + } + + /** + * Does this range need validity checking? + * @return is checking required? + */ + bool isValidityCheckRequired() const + { + return m_isCheckValidityRequired; + } + private: /** * parent text buffer * is a reference, and no pointer, as this must always exist and can't change */ TextBuffer &m_buffer; /** * Start cursor for this range, is a clever cursor */ TextCursor m_start; /** * End cursor for this range, is a clever cursor */ TextCursor m_end; /** * The view for which the attribute is valid, 0 means any view */ KTextEditor::View *m_view; /** * This range's current attribute. */ KTextEditor::Attribute::Ptr m_attribute; /** * pointer to the active MovingRangeFeedback */ KTextEditor::MovingRangeFeedback *m_feedback; /** * Z-depth of this range for rendering */ qreal m_zDepth; /** * Is this range's attribute only visible in views, not for example prints? */ bool m_attributeOnlyForViews; /** * Will this range invalidate itself if it becomes empty? */ bool m_invalidateIfEmpty; + + /** + * Should this range be validated? + * Used by KateTextBlock to avoid multiple updates without costly hashing. + * Reset by checkValidity(). + */ + bool m_isCheckValidityRequired = false; }; } #endif